Deep Dive Open Questions
Zero operating questions, security contracts, spike plans, and resolved design decisions.
Review target: 00-final-recommendation.md, checked against 01–06 and the fixed founder constraints. Decisions already made are treated as constraints; the questions below concern their consequences and implementation viability. An item marked BLOCKER must be resolved or pass a spike before feature implementation starts.
1. Contradictions & Stale Content
1.1 Zero is decided, but the synthesis still describes an engine bake-off
- BLOCKER — executive recommendation contradicts its own decision record.
00-final-recommendation.md— §1 "Executive recommendation" calls Zero the "primary architecture candidate," PowerSync a "mandatory benchmark/fallback," and Postgres authoritative "even if" PowerSync is compared. Seven paragraphs later, §1 "DECISION (2026-07-16): Zero, not PowerSync" says PowerSync is eliminated. Rewrite the opening stack as a committed Zero architecture; keep the eliminated comparison only as dated rationale. 00document metadata still says "Status: recommendation for architecture spike." Change it to "architecture decision; Zero viability gates pending," so passing a spike is not confused with choosing an engine.- Residual PowerSync conditional 1 — target diagram.
00-final-recommendation.md— §3 "Target system architecture" says "Zero or PowerSync local database" and "Zero query/mutate endpoints OR PowerSync connector API." Replace both branches with Zero and show the app-owned message outbox as a distinct local store. - Residual PowerSync conditional 2 — storage semantics. The same §3 diagram says "SQLite native / IndexedDB or OPFS web," conflating Zero's native
kvStoreand web IndexedDB path with PowerSync's queryable SQLite/OPFS model. Rewrite the diagram to avoid promising a directly queryable client SQLite database on every platform. - Residual PowerSync conditional 3 — package boundary.
00-final-recommendation.md— §4 "Proposed structure" definessync-schema/as "Zero or PowerSync-facing." Use the Zero-specific package names (zero-schema,zero-queries,zero-mutators); a lowest-common-denominator engine abstraction would discard chosen-engine capabilities without preserving a real fallback. - Residual PowerSync conditional 4 — range ownership. §7.1 "Message-window model" says range metadata may be implemented by application code or "inferred from the selected sync engine." There is no remaining selection. Specify which coverage facts Zero exposes and which local-only coverage/anchor facts the application must own.
- Residual PowerSync conditional 5 — send branch. §7.7 has headings "If Zero wins" and "If PowerSync wins." Make the Zero path normative, delete the PowerSync implementation branch, and specify the two-store crash protocol between the local outbox and Zero.
- Residual PowerSync conditional 6 — candidate ranking. §8 "Sync-engine decision" still contains "Candidate ranking," "Choose Zero if," a complete PowerSync fallback recommendation, and InstantDB as an active alternative. Convert this section to "Chosen engine: accepted risks and mandatory mitigations."
- Residual PowerSync conditional 7 — Postgres rationale. §8 "Postgres vs Mongo" says PowerSync can still be benchmarked and Postgres does not pre-decide Zero. Both Postgres and Zero are already decided; this sentence is stale.
- Residual PowerSync conditional 8 — Stage 0 dual implementation. §14 "Stage 0 — architecture spike" requires the same slice in Zero and PowerSync. Retain the scenarios, but run them as Zero viability gates rather than spending time on an eliminated engine.
- Residual PowerSync conditional 9 — winner rule. §14 "Winner rule" still contains "Choose Zero if" and "Choose PowerSync if" branches. Replace them with explicit pass/fail thresholds and a founder escalation if Zero fails a non-negotiable constraint.
- Residual PowerSync conditional 10 — risk mitigation. §16 "Major risks and mitigations" says to benchmark PowerSync as fallback and includes a PowerSync-only history-plumbing risk. Delete the PowerSync row and replace generic "engine abstraction boundaries" with deliberate Zero containment and data-export boundaries.
- Residual PowerSync conditional 11 — final stack. §17 "Recommended stack to take into the spike" says "Primary sync trial: Rocicorp Zero" and "Fallback sync trial: PowerSync." The implementation stack should say "Sync: Rocicorp Zero."
- Residual PowerSync conditional 12 — confidence. §17 "Recommendation confidence" calls Zero only a medium-confidence final engine pending comparison and calls PowerSync the fallback if offline writes dominate. Offline writes no longer dominate by decision; retain maturity concerns as accepted Zero delivery risk, not an engine fork.
- Residual PowerSync conditional 13 — design principle. §17 "The design principle to retain" says "Zero or PowerSync owns data." Rewrite as "Zero owns synced reactive data; the app-owned outbox owns pending message-send commands."
- Residual PowerSync conditional 14 — auth executive.
04-better-auth-evaluation.md— §"Executive take" conditions Better Auth on either Zero or PowerSync. Better Auth and Zero are both decided; recast the document as an adopted boundary with integration gates. - Residual PowerSync conditional 15 — auth fit table. §"Fit against requirements" contains active PowerSync and Convex integration recommendations. Label them historical alternatives or remove them from implementation-facing guidance.
- Residual PowerSync conditional 16 — auth session variant. §"Session and sync design" contains a complete "PowerSync variant." Delete/archive it; the Zero token-refresh contract is the only active design.
- Residual PowerSync conditional 17 — auth conclusion. §"Provisional recommendation" says "Zero or PowerSync remains responsible." Rename the section "Adopted boundary and release gates," and make Zero normative.
- Residual PowerSync conditional 18 — theme preference.
06-ui-theming-frameworks.md— §C4 "Runtime theme switching on both platforms" says profile themes sync through "Zero/PowerSync." Replace with Zero. - Residual PowerSync conditional 19 — landscape conclusion.
03-sync-engine-landscape.md— §"Postgres vs Mongo implications" and §"Shortlist for a Discord-like app" rank PowerSync first, condition the choice on Mongo, and end in a PowerSync-vs-Zero decision fork. The factual comparison can remain as historical research, but the conclusion must be stamped SUPERSEDED BY FOUNDER DECISION 2026-07-16.
1.2 Zero status and capability descriptions are stale or internally inconsistent
03-sync-engine-landscape.md— §1 "Rocicorp Zero" says Zero is alpha/pre-beta and targeting beta in late 2025/early 2026. Current official status says Zero has been GA and fully supported since March 2026, with 1.0 the first stable release; the corpus is using an obsolete roadmap snapshot.03-sync-engine-landscape.md— §1 says Zero SaaS was invite-only as of late 2025. That is historical, not a current operating assumption; the self-hosted infrastructure topology must be validated against current Zero support and operational requirements.00-final-recommendation.md— §1 "DECISION" says long-duration offline writes were PowerSync's "only decisive advantage." The same file's §8 and §16, plus03-sync-engine-landscape.md§§1–2, identify RN maturity, production history, operational maturity, and dataset headroom as independent advantages. Do not reopen the decision, but state that these are accepted risks that the Zero spikes must retire.03-sync-engine-landscape.md— §1 cites an approximately 40 MB client working-set guideline from an HN comment, not stable official product documentation. Current Zero TTL behavior intentionally evicts inactive queries and caps TTLs at ten minutes; replace the folklore number with measured budgets on target devices.05-shared-typescript-domain-architecture.md— §"Decision" says Zero owns a generic "mutation queue." Zero queues only whileconnecting; it rejects writes indisconnected,error, andneeds-auth. Rewrite this as "short-glitch optimistic queue," with the durable message outbox explicitly app-owned.05-shared-typescript-domain-architecture.md— §"React binding strategy" permits general "queued actions" in Zero-synced or local tables. Narrow this to drafts, local presentation state, and the single durable message-send outbox exception.
1.3 Offline scope contradicts itself
00-final-recommendation.md— §1 "Product stance on offline-first" limits the exception to message send and says everything else requires connectivity. §7.7 describes operations supported during "long offline periods," while §1 describes only brief tunnel/elevator failures. Default: yes for messages only, with an explicit "queued on this device" state, possible later rejection, and a seven-day automatic retry horizon before it becomes an unsent draft.00-final-recommendation.md— §11 "Learner experience" allows lesson completion to queue offline. This violates the decided message-only outbox and must be removed.00-final-recommendation.md— §13 "Reconnect" says pending "commands" resume automatically. Change the plural generic promise to pending message sends only.01-trw-chat-spec.md— introduction and §7 "Optimistic sends, retries, dedupe" describe a generic offline-first mutation queue. The useful evidence is message-specific; narrow the recommendation rather than generalizing it.02-trw-platform-spec.md— §A4 "Disconnect / reconnect / offline" verdict and §"Top lessons" recommend a generic client mutation outbox and a new resumable sync protocol. Those recommendations are superseded by Zero except for the message outbox and ephemeral presence transport.
1.4 The corpus alternates between Zero and an app-built second sync engine
01-trw-chat-spec.md— §10 "What to keep" proposes generalizing replay cursors into full resumable sync and calls EventV2 the template for a sync protocol. These are legacy lessons, not implementation instructions after selecting Zero.02-trw-platform-spec.md— §A4 verdict and §"Top lessons" call for a generalized CDC entity protocol, durable per-user log, client store, and mutation outbox. Keep CDC/outbox for server-side side effects, search, audit, and notifications; do not build a competing client data protocol.00-final-recommendation.md— §3 "Architectural invariants" and05— §"Layer 3: Zero as the entity store" correctly say Zero is the only synced reactive entity store. Add an explicit prohibition on a second replay/entity protocol so the legacy documents cannot be misread.
1.5 Hot/cold history is described in mutually exclusive ways
03-sync-engine-landscape.md— §"Hybrid architecture" sends cold history through REST/RPC and optionally promotes it into a local cache. The same section says Zero collapses hot and cold into one abstraction. Both cannot be true when cold rows are excluded from Zero's publication.- Required rewrite: distinguish client partial sync from server replica scope. Zero queries limit what reaches a client, while
zero-cachestill holds the whole published subset in SQLite; official guidance currently recommends less than roughly 100 GB of published backend data.
1.6 Other corpus contradictions
- No shared rendering vs shared UI package.
00§1 correctly bans shared rendering, but §4 uses onepackages/ui/, and §16 says to share components selectively. Align with05:ui-web,ui-native, and shared contracts/tokens only. - Chosen UI path vs optional paths.
06-ui-theming-frameworks.md— §C2 still offers Terrazzo or Style Dictionary, and §"Final recommendation" offers a second DaisyUI-components path. The decided constraints select Terrazzo and shadcn-style owned components on Base UI; label the alternatives rejected. - Better Auth selected vs provisional.
00§9 says "Adopt Better Auth provisionally." Rewrite as selected identity infrastructure with mandatory security and mobile integration gates. - Better Auth organization ownership is ambiguous. Default: do not make Better Auth Organization rows canonical; app tables own invitations, memberships, owners, and roles.
- Legacy realtime "always CDC" claim is false.
02-trw-platform-spec.md— opening "Golden dataflow invariant" says writes always traverse Mongo change streams.01-trw-chat-spec.md— §0 and02§A2 say chat EventV1 is emitted from model helpers/serf, not sink CDC. Correct "always" so the lesson is evidence-based. - Identity and cursor are conflated. Keep client-generated identity and a separate authoritative order cursor, even if both are lexically sortable.
- Permission materialization is both discouraged and prescribed. Default: relational filters plus scoped versions first; materialize only if the permission/fan-out spike proves it necessary.
- Presence is both durable and ephemeral. Remove presence from the Zero dataset.
- Live rooms are both HLS-only and interactive-first. Rewrite the top lesson to match the decided interactive-first model.
- DR, sharding, and upgrades are deferred too late. Move topology, failover, replica rebuild, and rolling upgrade proof into Stage 0.
- "Reconnect never clears" is too absolute. Rewrite as "ordinary network reconnect never clears valid data"; destructive reset is an explicit, instrumented recovery path, and it must never delete app-owned drafts/outbox storage.
2. Zero Hard Questions
2.1 What exactly is the syncable dataset?
"messages" is not a dataset boundary. 00 §5 defines one messages table, while §16 promises a bounded hot publication. Specify the exact published tables, columns, partitions, expected row counts, average row widths, growth rate, retention, and three-year size.
- Zero's client query limit does not limit
zero-cachereplica size. Every view-syncer hydrates from a SQLite replica of the published Postgres subset; fast attached storage is a design assumption. A million-row demo does not validate a multi-tenant corpus that may become hundreds of millions of messages. - A single shared
messagestable in the publication eventually puts all tenants' history in every serving replica. Row-level client permissions do not reduce that replica. The corpus never supplies a capacity model: SILENT. - Default: model a physical
messages_hotpublication and an archive store from day one, with a 90-day initial hot target subject to spike results. Archived rows are served by an authorized history API and are not copied into Zero's entity store. - Alternative A is publishing full history until approaching 100 GB. This preserves Zero-native
around(...)but leaves a disruptive migration exactly when scale pressure is highest. Reject unless the capacity model shows at least three years of 2× headroom. - Alternative B is per-tenant or tenant-cohort Zero deployments. Current Zero app metadata refers to a single shard
0with future sharding; built-in row-level tenant sharding must not be assumed. Multiple deployments imply routing, multiple sockets/Zero instances for users in several servers, cross-server home aggregation, and independent migrations. - How does a user who belongs to 50 servers connect if those servers span shards?
05— §"Layer 5: the headless AppRuntime" assumes one Zero handle. The corpus is SILENT on multi-Zero-client orchestration.
2.2 Can zero-cache be operated to the product's SLO?
00§8 lists "operational maturity" as a risk. The self-hosted infrastructure topology still needs a concrete region layout, ownership, paging, cost and support-escalation design.- Current self-hosting uses one replication-manager and N horizontally scalable view-syncers. Define whether a single replication-manager is acceptable and what its failover procedure is.
- Fast local disk is not optional at scale. Specify NVMe/IOPS, disk headroom for replica plus WAL plus temporary indexes, and double-space requirements for vacuum.
- Default SLO: RPO 0 for committed Postgres data; RTO under 30 minutes for one serving cell; no global client reset.
- What bounds logical-slot WAL growth when zero-cache is unhealthy? Define lag alarms, disk alarms, slot recreation authority, and the point at which the app degrades to read-only.
- Postgres failover is not automatically safe for logical consumers. PostgreSQL 17+ failover slots require explicit provider configuration and Zero opt-in; provider support varies. The chosen database provider and failover mode are SILENT.
- What happens when a publication changes? Current Zero documentation warns that changing publications resyncs the replica and can cause lag/downtime; a new app ID is the no-disruption route. This makes "we will exclude cold data later" an operational migration project, not a config toggle.
2.3 How many query windows can the system afford?
01-trw-chat-spec.md— §6 keeps up to 60 warm channels.00§7.6 retains a warm pool, and §13 preloads recent data, but no query budget is defined. SILENT: active queries per foreground channel, per warm channel, per server list, per thread, and per course screen.- Zero queries consume client metadata and server-side materialized state; inactive query TTLs default to five minutes and are capped at ten. A React
<Activity/>pool is not a license to keep 60 server subscriptions live indefinitely. - Every present-tail subscription must advance when its channel receives a message. Historical windows should not remain live-tail subscriptions; use a complete one-shot hydration with short/no TTL where possible.
- Permission joins are part of every hydration plan. A messages query that joins membership, roles, overrides, and entitlements can scan far more rows than it returns. Require Zero
analyze()evidence withreadRowCounta small single-digit multiple ofsyncedRowCount. - Fan-out and hot-channel behavior are SILENT. Seed "celebrity" channels, not only uniformly distributed traffic; one 50,000-viewer channel can be harder than 1,000 quiet channels.
2.4 How are revoked rows actually removed?
00§6 "Sync visibility" states the requirement but not the mechanism. Default: put membership, role assignment, channel override, ban, and entitlement state in authoritative relational rows used by named-query filters.- Zero's auth revalidate and auth retransform intervals default to unset. Without explicit values, logout/session revocation and out-of-band role changes can remain stale on an open socket. Default target: online revocation P95 under 2 seconds and P99 under 5 seconds.
- Removal must cover the entire reachable graph. Test messages, attachments, reaction rows/counts, reply parents, thread metadata, read state, course rows, cached search snippets, and notification previews.
- An offline device cannot receive a revocation. No sync engine can remotely erase an offline disk; this must be a stated security/product contract.
- Default: normal communities accept "revoked on next successful sync"; high-sensitivity servers may opt out of durable local storage or use shorter/local retention.
2.5 Does the client working set fit a Discord-scale UI?
- SILENT: maximum cached servers, channels, tails, disjoint windows, threads, course metadata, and attachments per device tier. "Recently active" is not an eviction algorithm.
- Query TTL can remove inactive synced rows while the app remains open.
00§7.1 assumes several durable disjoint windows and §13 promises a retention policy, but it does not reconcile those promises with Zero's query lifecycle. 00§7.1 proposeschannel_message_rangesbut is SILENT on whether those rows are server-authoritative, client-local, or inferred. Default: use Zero query identity, completion status, and returned boundary cursors; persist navigation anchors only.- Default device budget: on a mid-range Android device, total app PSS below 250 MB, Zero-related JS heap below 80 MB, no more than 10 retained channel tails and 3 explicit historical windows, subject to measurement.
2.6 Is React Native support production-ready for this lifecycle?
00§8 says official Expo support exists, but "official" is not a mobile reliability result. Current adapters includeexpo-sqliteand fasterop-sqlite; the latter requires prebuild/development builds and does not work in Expo Go.- Choose the adapter now. Default:
op-sqlitefor release builds if it passes crash/corruption tests; keepexpo-sqliteonly as the lower-complexity fallback. - SILENT: behavior under iOS suspension, Android process death, background socket termination, low-storage eviction, SQLite corruption, OS backup/restore, and app binary upgrade.
- Test cold restore with a realistically large
kvStore, not an empty demo. Pass only if cached identity and the last channel paint within 500 ms on target mid-range hardware. - Local data encryption is SILENT. Better Auth credentials live in SecureStore, but Zero rows and the message outbox can contain sensitive content on disk.
2.7 How does auth refresh work on long-lived sockets?
04-better-auth-evaluation.md— §"React Native + Zero" says refresh the JWT and reconnect in place, but supplies no token TTL, refresh threshold, concurrency control, or failure state machine.- Default: 10-minute service JWT, refresh at 60–90 seconds before expiry with jitter and single-flight deduplication; use
zero.connection.connect({auth})without changinguserID. A refresh failure moves writes toneeds-auth. - Web cookie refresh is not automatically equivalent to token refresh. The long-lived WebSocket may retain the authentication context from its opening request.
- Key rotation must overlap old and new JWKS keys long enough for live clients. Never recreate a Zero instance merely because the token string changed.
2.8 What is the exact durable message-outbox boundary?
00§7.7 lists states but not the atomicity model. The app outbox and Zero storage are separate local systems; there is no transaction spanning them. "Write outbox, then mutate Zero" has an unavoidable crash gap that must be closed by idempotency.- Default protocol: persist the complete command first in an app-owned encrypted SQLite table; only then render its pending projection and invoke the Zero mutator. Never delete the outbox record merely because the optimistic mutation returned locally.
- A unique constraint on a time-partitioned messages table may not enforce
(channel_id, client_nonce)across partitions. Default: use an unpartitionedmessage_send_receiptstable with unique(channel_id, client_nonce). - The outbox command needs a schema version. Native clients may remain offline across an app/schema upgrade; dispatch must migrate old commands or show a recoverable failure without losing the user's text.
- Default: auto-dispatch for seven days, then convert to an editable unsent draft; retain server idempotency receipts for at least 30 days.
- Define retry taxonomy precisely. Network/5xx/timeouts retry with capped exponential backoff; auth waits for refresh; permission, deleted channel, content policy, and attachment rejection become user-actionable terminal states.
3. Data Model Open Questions
3.1 Tenancy and database topology
00§5 "Identity and tenancy" says every tenant row carriesserver_id, but §14 defers sharding and residency. SILENT: whether all servers share one Postgres cluster, whether enterprise tenants can be isolated, and how a user spanning servers is routed.- Default: one shared schema and shared Postgres cluster initially, with mandatory
server_idon tenant rows and composite indexes beginning withserver_idwhere access patterns require it. Do not use schema-per-tenant. - Direct messages do not fit "a channel belongs to exactly one server." Default: separate global
dm_conversations/dm_membershipsfrom server-owned channels rather than nullable tenant rules on every channel query.
3.2 Message identity, ordering, and partitioning
00§5 requires separateidandsort_idbut does not define who assignssort_idor its total-order semantics. Default: client-generated UUIDv7/ULID identity plus server-assigned fixed-width lexical order cursor based on commit sequence, withidas tie-breaker.- Zero maps Postgres numeric types to JavaScript
number; do not expose a 64-bit sequence that can exceed safe integer precision without an explicit string encoding. This representation decision is SILENT. - Default: keep the bounded
messages_hottable unpartitioned initially and range-partitionmessages_archivemonthly; partition hot data only after measured pressure. - Default: synced tombstone retaining ID/author/time/thread linkage; encrypted or access-restricted audit revision in a separate non-client table; blobs deleted by retention policy.
3.3 Replies and threads
- Lifecycle semantics are SILENT. Specify whether replies must be same-conversation, whether chains may nest, maximum ancestry depth, and cycle prevention.
- A reply parent may be deleted, archived, inaccessible, or outside the current window. The client needs a stable tombstone/snippet contract and an authorized
byIds/archive lookup that cannot leak hidden content. - Thread read state and notification state are different. Model a normal
channel_read_statefor the thread plusthread_subscriptionfor follow/mute preference.
3.4 Reactions and aggregates
00§5 normalizesmessage_reactionsbut never specifies how counts reach a client that does not sync every reaction row. Current Zero roadmap still lists aggregates/group-by as future work.- Default: add
message_reaction_counts(message_id, emoji, count, version)rows maintained transactionally with normalized reactions. This avoids rewriting the whole message row for every reaction. - Define idempotent add/remove semantics using unique
(message_id, user_id, emoji). Count maintenance must never go negative and must survive replay, duplicate requests, moderation deletes, and archive moves.
3.5 Read state and unread summaries
00§5 proposes one(user_id, channel_id)row. Create rows lazily only after first view and define retention after membership removal.- Multiple devices can race read cursors. Server updates must be monotonic (
maxby order cursor), except an explicit "mark unread" operation that needs a separate override field. - Default under current constraints: keep a device-local presentation cursor but do not replay it; authoritative unread may reappear on reconnect.
00§5 callsmessage_mentionsoptional even though unread mentions and notification routing depend on it. Make it required.- Default product contract: unread dot derived from channel tail cursor versus read cursor, exact mention count, and exact message count only for the active channel if requested.
3.6 Attachments and blobs
- Default synced fields: attachment ID, owner/message ID, media type, byte size, dimensions/duration, blurhash, processing state, moderation state, and stable variant descriptors.
- Do not sync expiring signed URLs. Resolve authorized download URLs through a short-lived API or CDN signing endpoint.
- Original filenames, EXIF, moderation evidence, virus-scan detail, and storage keys may be sensitive. Put non-client fields in separate unpublished tables or exclude columns via publication.
- Offline-readable media is SILENT. Metadata may sync while content does not; explicit downloads need a separate encrypted file cache, quota, revocation disclaimer, and eviction UI.
3.7 Search and unsynced history
00§7.9 says Postgres FTS may be enough but supplies no corpus size, language, ranking, typo tolerance, moderation, or latency requirements. Current Zero roadmap lists first-class text search as future work, so search is an API regardless of the sync choice.- Default: start with Postgres FTS for hot and archived messages behind an authorized API; add an external engine only when measured relevance or latency fails.
- Search results must carry source tier, message ID, channel ID, order cursor, safe snippet, and visibility version. Reauthorize both at search time and when opening the result.
4. AuthZ Open Questions
4.1 Capability manifest shape
00§6 provides a TypeScript union, not a manifest. A manifest also needs resource kind, action, default, allowed override scopes, entitlement dependency, owner/admin bypass behavior, moderation-mask behavior, audit category, and stable deprecation/version metadata.- Default: define one code-generated registry whose keys produce the TypeScript union, fixtures, admin UI labels, and policy documentation. Storage may compile to bitsets later, but bit positions are never the public/domain API.
- Capability reasons are too coarse. "role-denied" does not identify which role/override/policy won. Return a safe client reason plus structured server-only trace for audit/debugging.
- Workers, LiveKit webhooks, importers, moderation services, and future bots need first-class service principals. SILENT — default: short-lived credentials, explicit tenant/resource scope, no inherited human roles, and immutable audit attribution.
- "Audited" is not a schema. SILENT — default: append-only audit events with actor/service principal, tenant, action, resource, normalized before/after delta, policy version, request ID, reason, timestamp, and a declared retention class.
4.2 Permission precedence rules
The platform uses layered, order-sensitive capability algebra stored as human-readable capability_key + effect rows. Evaluate one channel in exactly one server, with explicit inputs only, in this order:
- Start with
server.default_permissions. - Apply every applicable server-role override in TRW rank order.
- Apply applicable user-attribute overrides. Attributes are a first-class cross-server user dimension: subscription tiers, staff cohorts, and similar platform-wide facts can modulate permissions inside any server/channel.
- Apply channel-level overrides in order: channel default, applicable channel roles in rank order, then applicable channel attributes.
- Apply the timeout/moderation mask (
ALLOW_IN_TIMEOUT) after ordinary grants. - Apply the bounded server-owner/platform-privileged safe bypass. No bypass defeats account disable, ban, server suspension, legal hold, or hard entitlement/quota gates.
- Apply required entitlements after the permission result. A permission grant cannot manufacture a paid entitlement; feature flags may remove access but never grant it.
Use one shared pure-TypeScript calculator on client and backend. It accepts the server defaults, rank-ordered roles, user attributes, channel overrides, moderation state, owner/privileged state, typed configuration, entitlement facts, and manifest version as explicit values; it must not reach through a global singleton such as appClient.attributes.get. Unknown capability keys fail closed.
4.3 Entitlement propagation through synced queries
- Default: query filters join active internal entitlement projections; JWTs contain identity, not live entitlements.
- Time-based expiry does not generate a database change by itself. An
expires_at < now()check captured in a query transform can remain stale. Maintain explicit active/grace/revoked projection state through scheduled jobs and reconciliation. - Entitlement loss can remove entire channels/courses and their child rows. Apply the same P95/P99 revocation SLA and graph-deletion inspection as role revocation.
4.4 Guests and anonymous access
- Better Auth supports anonymous users, but product behavior is SILENT across
00§9 and04"Fit against requirements." - Default v1: no anonymous writes and no guest membership rows. Serve marketing, invite, and public-course previews through a narrowly authorized HTTP path; create a normal account before opening member Zero data.
4.5 Moderation and retroactive visibility
- Moderation semantics are SILENT. Specify delete-for-self, author delete, moderator redact, legal purge, ban, timeout, quarantine, block, and shadow-spam behavior separately.
- Default: user-visible message becomes a tombstone; moderator-only original/revisions live in an unpublished audit table; legal purge destroys content and blobs while retaining the minimum non-content audit record allowed.
- Push notifications and search snippets are copies outside Zero. Revocation and redaction SLAs must include notification payload minimization, search deletion, CDN/object URLs, analytics, and support tools.
5. Product/UX Open Questions
5.1 Jump-to-message under query-driven hydration
- Default UX: keep the current window visible, show an anchored inline loading marker, and swap only after the target window is complete. Never blank the channel or destroy the present window while the remote request is pending.
- Targets: cached target visible in under 50 ms; hot remote target P95 under 750 ms web and 1.5 seconds mobile; archive target P95 under 2 seconds. At 3 seconds show retry/cancel while preserving context.
5.2 Unread badges without syncing all messages
- Recommended product contract: channel/server unread dot, exact mention count, and optional approximate/lazy message count. This can derive from synced channel tail cursors, sparse read cursors, and mention projections without syncing message bodies.
- Define muted channels, muted servers, thread follows,
@everyone, role mentions, self-authored messages, and mark-unread behavior.
5.3 Typing and presence transport
00§12 correctly excludes them from durable sync but names no replacement transport: SILENT.- Default: a separate Bun-compatible WebSocket gateway backed by Redis/Valkey ephemeral TTL sets. Authenticate with a short-lived Better Auth service JWT. No replay, no Postgres write, no Zero row, and no transactional guarantee.
- Coalesce typing, expire after roughly 8–10 seconds, cap visible typists, and rate-limit per user/channel.
- Do not use LiveKit data channels for global chat presence. LiveKit transport is appropriate only while a user is in a live room.
5.4 Notification routing
- Default pipeline: domain commit writes one notification intent; workers resolve recipients and preferences, dedupe/aggregate, reauthorize, then emit in-app, push, and email deliveries with channel-specific idempotency keys.
- Minimize sensitive push payloads. Private/high-sensitivity servers should send "New activity" without message content; revoked users must fail safely on tap.
- Multi-device delivery and read state need a canonical notification record plus per-device delivery attempts.
5.5 Course progress and video position
00§11 says progression is local and may queue offline, conflicting with the fixed outbox scope. Remove offline completion replay unless the founder explicitly creates another exception.- Do not write playback position to Zero every second. Default: send a coalesced authenticated API update every 15 seconds and on pause/background/exit; sync the resulting coarse
lesson_progressrow through Zero. - Completion needs a rule: manual, percentage watched, quiz pass, or server event. Default: typed per-lesson completion policy, server-authoritative transition, idempotent completion reward, and no client-granted role.
6. Delivery Sequencing
Spike 0 — freeze the architecture contract (1–2 days)
- Write one Zero-only architecture page replacing every active conditional identified in §1.
- Freeze terminology: Zero-synced store, local message outbox, ephemeral WebSocket plane, object/media plane, and optional archive history store.
- Freeze the v1 offline matrix per command.
- Pass: no implementation-facing document names an eliminated sync engine as a candidate or fallback, no generic offline queue remains, and each persisted store has one declared owner.
- Fail: any team member can reasonably infer that cold REST rows become Zero rows, that Zero provides durable offline writes, or that Better Auth Organization owns product memberships.
Spike 1 — prove Bun/Zero integration and history topology (week 1)
- Run the Zero query/mutate API under the selected Bun server framework while zero-cache runs as its separately versioned service.
- Build the proposed full-history and hot/archive publication shapes against synthetic production-width messages, reactions, memberships, roles, and attachments.
- Seed at least the projected year-three hot dataset; include one oversized tenant and realistic skew.
- Measure replica size, initial replication time, NVMe IOPS, WAL growth, query hydration, archive move cost, and around-message behavior.
- Pass: one topology has 2× three-year headroom under the 100 GB guidance per serving cell, hot remote jump P95 ≤ 1.5 s, archive jump P95 ≤ 2 s, steady replication lag P99 ≤ 2 s, and no entity duplication across Zero/history stores.
- Fail: viability depends on undocumented built-in tenant sharding, an undocumented dynamically aging publication filter, multiple Zero clients with no design, or a future Zero feature.
Spike 2 — zero-cache operations, migration, and DR (week 1–2)
- Deploy one replication-manager, at least two view-syncers, Change/CVR databases, sticky WebSocket load balancing, Litestream backup, and production-like NVMe.
- Kill each view-syncer, then the replication-manager; corrupt/delete a serving replica; interrupt backup restore; fill logical replication lag; rehome clients.
- Perform a Zero rolling upgrade in documented order and separately perform expand/backfill/contract schema migrations.
- Pass: RPO 0 for committed Postgres writes, one-cell RTO ≤ 30 minutes, no global client-store reset, no unbounded WAL growth, and actionable dashboards/alerts/runbooks.
- Fail: recovery requires manual client deletion, cannot preserve the logical stream across provider failover, or misses the SLO at projected dataset size.
Spike 3 — authorization and revocation correctness (week 2)
- Implement the frozen precedence algebra with membership, role, channel override, timeout, ban, owner, server suspension, and entitlement rows.
- Generate property tests and a truth-table fixture shared by client hints and server authority.
- Open clients on web/iOS/Android, sync complete entity graphs, then revoke through every input independently and in batches.
- Pass: forbidden writes fail immediately; connected row removal P95 ≤ 2 s/P99 ≤ 5 s; no inaccessible child rows remain; load stays within the query/fan-out budget.
- Fail: correctness depends on UI hiding, full app reset, JWT-embedded role state, or per-user/channel materialization that cannot update at target scale.
Spike 4 — query fan-out and hot-channel load (week 2–3)
- Model the full screen query set: server list, channel list, active tail, unread/mentions, thread metadata, member snippets, warm history, and course summaries.
- Load at least 10,000 concurrent clients, 50,000 active queries, 1,000 active channels, and a celebrity channel with 5,000 concurrent viewers.
- Inject 500 messages/second distributed and burst traffic; include reaction storms and role/entitlement changes.
- Pass: initial hot-tail hydration P95 ≤ 750 ms in-region, incremental message visibility P95 ≤ 500 ms, CPU sustained below 70% at target capacity, and bounded memory/CVR growth with documented scale-out triggers.
- Fail: a single message or permission change causes tenant-wide scans, celebrity channels collapse one view-syncer, or query TTL churn creates repeated full hydrations.
Spike 5 — React Native lifecycle and working set (week 3)
- Use physical mid-range iOS and Android devices with the chosen SQLite adapter and production release builds.
- Seed 50 servers, 2,000 channel metadata rows, 10 retained tails, 3 disjoint windows, large member/reaction graphs, and realistic attachment metadata.
- Pass: cached channel paint ≤ 500 ms, total PSS ≤ 250 MB, Zero JS heap ≤ 80 MB, no corruption/data crossover, and reconnect does not block scrolling/composing.
- Fail: requires Expo Go, loses cached rows on ordinary OS kill, exceeds memory budget with the bounded dataset, or rebuilds Zero on token refresh.
Spike 6 — durable message outbox fault matrix (week 3–4)
- Implement the local SQLite outbox, pending projection, stable nonce/ID, server receipt table, Zero mutator, authoritative acknowledgement, and compaction.
- Fault-inject process death before/after every local write, Zero call, network send, Postgres receipt insert, message insert, response, observed sync row, acknowledgement write, and cleanup.
- Pass: zero lost accepted commands, zero duplicate canonical messages across at least 10,000 randomized runs, every terminal rejection preserves editable content, and restart converges without user action when retryable.
- Fail: correctness relies on timing, optimistic promise resolution, in-memory flags, or uniqueness local to one message partition.
Spike 7 — auth refresh and account isolation (week 4)
- Exercise Better Auth cookie web flow and native JWT flow across multiple token lifetimes while Zero and the ephemeral gateway remain connected.
- Verify
updateAuth/retransform behavior without Zero-instance recreation for the same user. - Pass: no full rehydrate on routine refresh, revoked sessions disconnect within the auth SLA, queued messages wait safely through
needs-auth, and physical storage inspection shows no cross-account data. - Fail: socket auth remains valid beyond policy, cookies refresh only after page reload, or a rejected account can dispatch the previous account's outbox.
Spike 8 — hot/archive jump, unread, and search UX (week 4–5)
- Implement present, first-unread, hot selected-message, archived selected-message, deleted target, unauthorized target, and search-result entry.
- Scroll across archive/hot boundaries while live messages arrive; return to present and back without destroying either anchor.
- Pass: meets §5 latency budgets, never blanks an existing window, never duplicates identity across source tiers, and unread summary traffic is O(channels/mentions), not O(all messages × members).
Spike 9 — message-list anchoring and media growth (week 5)
- Run the actual web and native virtualizers with prepends, images, embeds, font changes, keyboard transitions, thread opening, theme changes, and historical/live switching.
- Pass: no visible anchor displacement over 4 px at P95 for controlled-size content, no long task over 50 ms attributable to list reconciliation at P95, and 60 fps during ordinary scroll on target devices.
Spike 10 — ephemeral events, notifications, and course progress (week 5–6)
- Prove the separate WebSocket/Redis typing-presence plane, transactional notification intents, device delivery idempotency, and 15-second course-position API.
- Pass: no ephemeral event enters Postgres/Zero, duplicate worker delivery is harmless, push contains no forbidden content, and completion rewards issue once.
- Fail: notification correctness depends on direct handler emits, typing is replayed/durable, or video progress floods Zero mutations.
Do not begin Stage 1 feature breadth until Spikes 1–7 pass. Spikes 8–10 may overlap only after the underlying topology, authz, operations, RN, and outbox contracts are stable. A failed threshold triggers a written founder decision on scope/SLO/topology; it does not silently revive an eliminated sync engine or defer the problem to Stage 4.
7. Top 10 Decisions for the Founder
1. Adopt an explicit hot/archive history boundary
Recommendation: publish a physically bounded hot-message dataset to Zero and serve older history through an authorized read-only history/search API, starting with a 90-day target adjusted by the capacity spike. The tradeoff is losing Zero's single-abstraction story for cold jumps; the benefit is avoiding an architecture that crosses Zero's current ~100 GB guidance with no migration plan.
2. Accept and document the revocation limit of offline-readable data
Recommendation: promise removal within 2 seconds P95 while connected and on the next successful sync while offline; offer memory-only/short-retention mode for high-sensitivity servers. Instant offline reading and immediate remote erasure cannot both be guaranteed.
3. Use layered ordered-role and attribute permission algebra
Decision: store human-readable capability_key + effect rows and evaluate server defaults → rank-ordered server roles → user attributes → channel default/rank-ordered roles/attributes → timeout mask → bounded owner/privileged bypass, with entitlements gated afterward. Attributes remain the first-class cross-server dimension for subscription tiers, staff and similar platform-wide facts. Optional bitset caches are derived only. Port the calculator once to pure TypeScript with explicit inputs, typed configuration instead of magic IDs, fail-closed unknown keys, explainable results, and property tests.
4. Make the message outbox write-ahead and message-only
Recommendation: allow a message to be queued even when already offline, persist it before any Zero mutation, and acknowledge only from an authoritative server receipt/canonical row. Maintains a small encrypted local command system and rejection UX; avoids lost/duplicated sends without reopening generic offline mutation support.
5. Use shared-schema tenancy, not schema-per-tenant
Recommendation: start with one shared Postgres schema and explicit server_id, then introduce measured tenant-cohort cells rather than per-tenant schemas. Schema-per-tenant is incompatible with sane migrations, Zero publications, and cross-server users at this stage.
6. Choose dot-plus-mentions unread semantics
Recommendation: show an unread dot from tail/read cursors and exact mention counts; do not promise exact global message counts or durable offline-read reconciliation in v1. Avoids O(messages × members) projections and another offline-write exception.
7. Keep product memberships entirely app-owned
Recommendation: use Better Auth only for identity/session/provider concerns; app tables own invitations, servers, memberships, owners, roles, and all access lifecycle. Prevents dual membership sources and authorization drift.
8. Fund Zero operations as a first-class subsystem
Recommendation: self-host Zero self-hosted and staff explicit ownership for replication-manager, view-syncers, NVMe, Litestream, logical slots, CVR/Change DBs, upgrades and failover. The operating budget includes dedicated platform-engineering ownership and support escalation.
9. Use a separate ephemeral WebSocket plane
Recommendation: run typing and presence over a Bun-compatible WebSocket gateway with Redis/Valkey TTL state; reserve LiveKit transport for active live rooms. Putting ephemeral fan-out into Zero/Postgres would create write amplification, stale presence, and pointless replay.
10. Keep course progress online-authoritative and coarse-grained
Recommendation: sync coarse progress rows, send video position about every 15 seconds through an API, and require connectivity for completion/reward transitions. Preserves the message-only offline exception and prevents high-frequency Zero mutations or client-granted rewards.