# Innerloop: employee comms that reach shift workers without a company email

> Conversations are readable only by their participants, enforced by row-level policy. Creating one inserted the row and asked the database to return it in the same statement — but the creator does not qualify under that policy until the access rows are written, which happens after. The returned row f

Source: https://saascode.ai/inside/road-to-innerloop · Published: 2026-08-09 · Section: builds · Product: Innerloop (https://saascode.ai/products/innerloop)

---
The membership rule blocked its own first write.

Conversations are readable only by their participants, enforced by row-level policy. Creating one inserted the row and asked the database to return it in the same statement — but the creator does not qualify under that policy until the access rows are written, which happens after. The returned row failed the very policy protecting it. Every direct message and group conversation create died on `42501`. The fix does not touch the policy: mint the identifier before the insert, write the row without asking for it back, create the access rows, then read the row through a query the creator now qualifies for. Commit `a93e53a`. The invariant was right; the bootstrap sequence was wrong. The weaker fix — loosen the policy — was available and refused.

Innerloop is a mobile-first internal communications platform for employers: feed, announcements with per-recipient delivery proof, spaces, direct and group messaging, directory, recognition, learning paths, frontline tasks, and surveys — one deployment, each customer company an isolated tenant. The distinguishing property is delivery proof as a schema fact: an employer can verify, per row, who opened a policy notice or acknowledged a safety task. Not a reporting feature; the table design.

The Workplace from Meta closure framed the timing — read-only from September 2025, deleted June 2026. Meta's named migration partner does not serve below 250 employees, leaving the lower end of the displaced base without an obvious destination.

## Starting point

The scope was committed before any code was written. Named surfaces: a company feed; announcements with multi-channel delivery and acknowledgement tracking; spaces; direct and group messaging; a people directory with an org chart; recognition; learning paths; frontline tasks; surveys; mobile-first access for staff without a company email; and programmatic verbs for machine callers. Scope exclusions were named at the same time — payroll, full recruiting, public social networks, video conferencing — as definitions rather than deferrals. Three items were explicitly deferred: push through Apple's own service, ongoing record sync through a human-resources integration, and offline composing.

Role structure was decided structurally. The two platform-operator roles are reserved to the buyer who runs the deployment and are never assigned to a customer company. A customer company's owner is a distinct org-scoped role. The role constraint was altered before anything else touched the database.

Three architectural commitments carried through every later decision: every message and space write verifies the caller's access at the route, in the service, and again in row-level policy — three independent layers; live server push is spent only where a missed event is unrecoverable; the announcement tables carry locked names because a table serving a similar purpose already existed in the shared codebase for a different system.

## Reading the market

Two findings came from reading code rather than reading the web, and both changed the schema before the product was specified. The shared codebase already contained an `announcements` table — a platform-operator banner system that ignores the tenant column. Reusing it for employee announcements would have broken isolation across customer companies. The shared realtime helper is a broadcast transport, ephemeral by design; any message sent while a recipient was offline would simply never have existed.

The first conflict produced the name-locked tables — `content_announcements`, `content_announcement_deliveries`, `content_announcement_reads` — as a separate system. The second produced the decision to use persistent row-change events for messages rather than the broadcast transport.

Every platform in this category charges per employee per month against large frontline headcounts, which made the billing model obvious before any competitor's price was consulted. Blink's published rates are Core at $3.75 per user per month and Pro at $5.00, annual (joinblink.com/pricing, 2026-08-07). Where a workforce holds Microsoft 365 enterprise licences, a Viva Engage communications tier is available at $2.00 per user per month — the cheapest credible floor in the category. Beekeeper, following its acquisition by LumApps in July 2025, is no longer independently priced.

## The decisions that shaped it

**Live server push is rationed to two tables.** Row-change streaming was enabled for messages and notifications only. The feed refreshes by polling a count endpoint every thirty seconds; a "N new posts" banner appears from that poll or from a single shared per-tenant broadcast channel — never a per-user subscription. The reach dashboard polls. The reasoning is in the code: the feed is the surface every employee holds open — exactly where per-user connections multiply — and a thirty-second delay costs nothing there.

**The announcement tables were name-locked.** Three tables carry mandated names, with two earlier candidate names retired by directive so that no contributor could reintroduce the collision. The platform-operator banner table in the shared codebase was not modified or reused. Aggregate counters — delivered, read, acknowledged — live on the parent row, so the reach dashboard reads a constant number of rows rather than counting recipients.

**SMS fires only when three conditions hold simultaneously.** Delivery resolves per recipient in order: push if the recipient has a registered device and the tenant has web push on; email if the recipient has an address; then SMS — but only if the item is a critical must-read, the recipient has neither email nor a registered push device, and the tenant has opted in and holds its own messaging credentials. An in-app notification is always written regardless. SMS is the only channel that costs money per message and reaches a personal device; a gate that any single condition could open would eventually be triggered by a routine announcement.

**The delivery worker refuses unsigned work and cannot double-send.** The fan-out handler verifies the queue signature first and returns 401 on failure — closed by default, not open by default. Duplicate suppression exists at two independent layers: a deduplication identifier on the queue message, and a database uniqueness constraint on the triple of announcement, recipient, and channel, upserted with duplicates ignored.

**Billing is per seat, synced on roster change.** Roster mutations enqueue the tenant into a pending set; a scheduled drain coalesces them, so a two-thousand-row spreadsheet import produces one recompute rather than two thousand. The service targets a payment abstraction interface rather than any specific provider's SDK; a provider without native per-seat quantity degrades to a standard subscription.

**Every message and space write verifies access three times.** A guard for any authenticated employee of a given tenant did not exist in the shared codebase and was written for this product; thirty-seven route files use it. Using the reserved platform-operator guard for a customer operation would lock out the customer's own owner role.

**The feature-flag category set was current.** Five categories were added to the existing set: social, comms, engagement, learning, compliance. An analytics category was considered and dropped; reach analytics maps to comms and frontline tasks map to compliance. An open category list on a flag table is how a taxonomy becomes unenforceable.

**The product ships installable and honest about the boundary.** A service worker caches the feed and announcements read-only. There is no write queue and no background sync; offline composing is named as a future version in the service worker's own header comment. An offline composer that silently loses a post is worse than no offline composer.

## What fought back

The release was rejected on sight after design review. Twenty pages were built from per-page design references. Roughly twelve of the twenty replaced the design's class names with generic component-library equivalents — the surface colour became the generic primary, the border colour became the generic border. Each substitution is individually defensible. Collectively they produced a generic component library with a colour scheme applied on top, because a later styling step can tint a generic name but cannot recover a specific one. Every automated check was green. The repair audited thirty-five surfaces on five axes — sections and their order, copy, control types, class vocabulary, responsive behaviour — finishing with zero generic tokens in the scanned markup.

Three interaction classes were used in fourteen files and defined in none of them. The design's press, lift, and arrow micro-interactions were copied into the built pages as class names. Nothing declared what they did, so the feedback layer was a silent no-op — present in the markup, invisible in the browser. Two workers added the definition concurrently; the unscoped duplicate would have leaked the product's motion vocabulary into the platform-administration surface, which is required to stay on the shared baseline. The surviving definition is scoped correctly and respects reduced-motion.

The sanctioned path for updating interface text could add a string but never change one. The merge step that folds per-page text fragments into the seed file deduplicates by locale, namespace, and key. A fragment carrying a corrected value for an existing key silently skips it. The repair's copy corrections never landed through the canonical path; sixty-seven tuples were applied directly to the live database, and the sixteen changed rows were rewritten in the seed file so a future push could not revert them.

A safety net turned a constraint violation into a silent absence. The trigger that creates a profile row when an account is created inserts without a tenant identifier, and the profile table requires one. The trigger is wrapped in a handler that logs and returns normally — deliberately, so that a side-table failure cannot block sign-in. Nine demonstration accounts were created successfully and silently received no profile row; one whole role archetype vanished without an error anywhere. The seed now inserts the roster explicitly with its known identifiers.

Open limits at version one, all stated rather than discovered: the payment abstraction exposes a quantity at checkout but no method to push a mid-cycle quantity change to a provider; delivery emails use the generic notification template rather than branded per-type templates; there is no upload endpoint; spaces have no archive column; several administrative cross-tenant views show identifiers where a name belongs; phone-only sign-in does not mint a full session until the phone provider is enabled at deployment time; seventeen dependency advisories were logged rather than silenced.

## What shipped

The product covers the full scope committed at the start: feed, targeted announcements with per-recipient reach analytics, spaces, direct and group messaging, directory with a depth-capped org chart, recognition, learning paths, frontline tasks with acknowledgement tracking, surveys with anonymous result suppression below a five-person group floor, mobile-first access including an installable manifest, and programmatic access through bearer-authenticated machine-callable verbs.

Delivery is provable per person: one row per recipient per attempted channel carrying status and timestamps, one row per recipient carrying an opened time and an acknowledged time, with aggregates maintained on the parent record by triggers. Every route in the site map returned 200 or its expected redirect across the full surface; zero permission leaks including the boundary where a customer company owner is refused the platform administration panel; no critical or high findings in an authorisation battery run across thirteen roles and seventy-eight tables.

## See it

[See Innerloop →](https://innerloop.saascode.ai)

## Related reading

- [Employee Communication Platforms 2026: A Buyer's Guide for Frontline and Deskless Workforces](https://saascode.ai/inside/employee-communication-platforms-2026-a-buyer-s-guide-for.md)
- [One Vendor in This Category Will Tell You the Price](https://saascode.ai/inside/innerloop-vs-blink-vs-viva-engage-employee-communications.md)
