- Walked: 2026-07-02 against local real-BFF development infrastructure. The retained result names the suites but does not preserve an immutable commit, provider trace, or raw application report.
- Reconciled: 2026-07-18 against the current route, required durable store, engine, startup wire, continuity erasure/export composition, Next proxies, message-center component, shell badge, notification projection/write-back, two named Playwright specs, and two focused durability specs. The eleven-case unit suite passed freshly; the real-Postgres case collected as one skip without its database URL. This was not a fresh application run.
- Verdict: pass / deep for the durable BFF message-center core — current evidence deeply covers admin-scoped dispatch, policy gates, restart-safe inbox state, registered-link follow, explicit read/archive, drawer convergence, pair-scoped opt-out, durable delayed work, manual replay, exact-subject erasure, and continuity export. It does not prove an automatic scheduler, an upstream producer, or delivery by email, SMS, voice, push, or an OS tray.
- Current authority:
WALKTHROUGH/journeys/messages-center-deliver-and-unsubscribe.mdstarts at the same producer seam and ends before any external transport.
Result at a glance#
| Interaction | Proof · limit |
|---|---|
| Producer dispatch | Authority / proof: POST /v1/admin/customer-messages/dispatch requires admin:* or admin:messaging, validates the payload, applies crisis and quiet-hours policy, and awaits the candidate snapshot before acknowledging mutation. Real HTTP covers 401/403/400, accepted, delayed, suppressed, safety bypass, and duplicate branches. Limit: no named upstream domain event or provider adapter is exercised. |
| Inbox and browser model | Authority / proof: /messages uses same-origin Next proxies to read and mutate restart-safe recipient rows. One live browser case covers the complete customer loop; twelve fixture cases cover presentation and error states. Limit: the UI has no cursor beyond the newest 25, while search, filters, grouping, snooze, and topic display state are browser projections. |
| Receipts and follow | Authority / proof: direct dispatch durably creates queued; manual replay atomically adds sent and delivered; a registered-link follow durably appends clicked and rejects unknown/cross-user links. Limit: no provider callback or provider message id is received; the BFF authors every replay stage. |
| Inbox, bell, and drawer | Authority / proof: /v1/notifications projects customer-message rows from the same durable store, and /v1/notifications-write awaits read/archive mutation on that row. The live browser proves read-count convergence in both directions. Limit: the badge switches to customer-message unread only, whereas the drawer can also contain domain-feed items; this is not a complete aggregate notification count. |
| Pair-scoped subscription | Authority / proof: suppression is keyed by user × channel × category and shares the message snapshot. Real HTTP proves 422 suppressed, an unaffected channel, re-subscribe, and restored 201 dispatch; focused durability proof restores the opt-out after restart. Limit: active policy is not read back into the page's topic state after reload. |
| Quiet-hours replay | Authority / proof: a current preference can durably queue a non-essential dispatch; the admin replay route respects due time, rechecks crisis state, persists holds/failures, and commits message plus receipts before removing successful work. Limit: no timer, claim lease, or channel handoff invokes replay automatically. |
| Persistence and privacy | Authority / proof: production startup requires and hydrates the snapshot; ordered mutations publish only after persistence. Focused unit proof covers rollback, restart, races, durable holds, exact-subject erasure fences, and adjacent-user preservation; continuity export includes the same partition. Limit: the real-Postgres case is environment-gated, and profile-channel unbind is a separate operation. |
Evidence map#
Solid edges are exercised by the named application and durability suites. Dashed edges are the production meanings that the evidence does not earn. The diagram deliberately keeps the BFF inbox, delayed jobs, and notification drawer on one continuity authority rather than presenting them as independent systems. The combined outcome node keeps the three policy branches explicit without shrinking their labels into a wide mobile tree.
Proven observations#
Dispatch and policy decisions are real HTTP boundaries#
- The producer route accepts a bounded message id, recipient, channel, category, priority, subject, body, at most ten registered deep links, locale, sender, thread, and optional sent time. A customer bearer cannot forge a row; only the two messaging-admin scope forms reach dispatch.
- Crisis policy runs before quiet hours. An active non-essential crisis frame
returns
202 dispatch.suppressed.crisis_frameand writes no message. A critical incident bypasses both crisis suppression and a current quiet-hours window withdispatch.bypassed.safety_critical. - Quiet hours durably store an eligible dispatch in the same snapshot as inbox and suppression state; the inbox remains empty until an admin calls replay-due at or after the computed window end. Replay evaluates crisis state again and durably retains a held job with an incremented attempt count and last error.
- Normal dispatch enters the customer message-center engine. It rejects a
duplicate id and a suppressed user/channel/category key, records recipient
ownership, and creates a single
queuedmarker. The route awaits the snapshot before returning201, but makes no network call to an email, SMS, push, voice, webhook, or in-app transport adapter.
Acknowledged message state is durable and fail closed#
- Deployable startup calls
requireDurableCustomerMessageCenterbefore wiring the database-backed snapshot and hydrates that snapshot before traffic. A missing snapshot becomes authoritative empty state; production-shaped synchronous mutations and an unavailable sink are rejected. - Each durable mutation clones the last committed center, applies the candidate, advances global and per-subject revisions, persists, then publishes. The serialized mutation tail orders concurrent sends, reads, archives, opt-outs, replay transitions, and deletion; a failed save leaves the visible committed view and revision unchanged.
- The snapshot restores messages, ownership, receipt/read/archive fields, suppressions, delayed dispatches, revisions, and deleted-subject fences. The focused eleven-case suite proves failed-send and failed-read rollback, atomic multi-receipt discard, restart recovery, exact-subject erasure, adjacent-user preservation, send/delete ordering, and durable replay failure handling.
- A gated real-Postgres case repeats full-partition restart recovery and composed erasure-fence persistence. It is skipped without the configured database URL, so the source capability is stronger than the retained environment evidence on this page.
The live browser leg uses the real customer path after assisted seeding#
- The browser suite has thirteen cases. Its first case creates a customer
session and access-token cookie, derives the exact token subject, seeds two
rows through the real admin route, then opens
/messagesthrough the natural authenticated Next proxy. The producer seed uses a test-generatedadmin:messagingdevelopment bearer; customer reads and mutations use the browser session. - That live case renders both rows, searches, opens the real queued receipt,
follows a registered Tara URL through the Next proxy, verifies
clickedread-back, marks one row read, opens the drawer, marks the other row through notification write-back, archives it, opts out, proves suppression, re-subscribes, and renders the restored dispatch. - The other twelve browser cases intercept
/api/messages/*to exercise deterministic component states: fixture receipts, unified filters, local snooze, optional-topic display, mutation payloads, inbox/action errors, and empty state. They strengthen the UI state machine without becoming additional real-BFF lifecycle runs. - The pure-BFF suite has sixteen cases. Besides auth and validation, it covers lifecycle and idempotent archive, policy delay/replay/hold/bypass, exact-pair suppression and restoration, dedupe, owner isolation, and a 28-row snapshot whose default window contains the newest 25 and explicit window the newest 5 while total/unread aggregates remain complete.
Receipt stages describe BFF state, not transport evidence#
- A direct
sendMessagealways begins withqueuedand notesenqueued. The live browser case verifies precisely that stage; its richer sent/delivered fixture rows are presentation fixtures, not live provider observations. - Successful replay builds one candidate that sends the message, immediately
records
sentanddeliveredwith the same replay timestamp, removes the delayed entry, and persists before publication. Its note says “through channel adapter,” but no adapter is called in that method or route. - Replay-gate exceptions become durable holds. A semantic send failure is durably retained with its reason, and a snapshot failure leaves the original delayed entry queued without exposing an inbox row.
- Link resolution enforces owner and registered URL, then durably records
clicked. Following does not setreadAtUnixSeconds; only the explicit read action does. - The browser projection maps clicked and opened stages to one
openeddisplay state and copies free-form receipt notes into aproviderMessageIdmodel field. The rendered receipt list does not display that field, and the BFF has not received an actual provider identifier.
Inbox and notifications converge because they share one store#
/api/messages/inbox, mark-read, archive, unsubscribe, re-subscribe, and follow are thin Next proxies to/v1/customer/messages/*; they do not maintain another message database. Every BFF mutation awaits its durable counterpart and maps snapshot failure to503and an erased subject to409./v1/notificationsreads the same store's newest 25 unarchived rows, maps each tocustomer-message:<encoded-id>, and combines them with authorized domain-feed notifications. Its customer-message action opens/messages, not the message's registered domain deep link.- Notification
mark_readawaitsmarkReadDurably; dismiss awaitsarchiveDurably; mark-unread is rejected with 409. The drawer emits an inbox change event so the shell refetches or adopts the message unread count. - The shell separately requests
/api/messages/inbox?limit=1. If successful,customerMessageUnreadCountreplaces the supplied general notification count. The observed2 → 1 → 0convergence is exact for the seeded message rows, not proof that the badge totals every item visible in the combined drawer.
Subscription authority and browser presentation are not the same state#
- The engine stores suppression under
userId::channel::category, and the durable wrapper preserves it with the message partitions. The BFF suite proves an email/reengagement opt-out does not block in-app/reengagement, and that clearing the exact key restores later email dispatch. - The page's manage form exposes all five server channels and all ten categories
and sends the selected pair. Its local
topicsarray updates after success, but the inbox response contains no current suppression policies. Reloading therefore resets the topic presentation even while server suppression remains active in the current process. - Row-level Stop topic blocks categories marked required in the browser model. The general manage form does not apply that required-topic guard, so it can submit those same categories to a server that currently accepts them.
voiceexists in the server channel contract, but the shell display model has no voice channel and maps it towebhook. Neither named suite covers that presentation mismatch.- The route and store emit no separate audit event for dispatch, replay, read, archive, follow, unsubscribe, or re-subscribe. Persisted revisions order the continuity snapshot; they are not an operator audit ledger.
Continuity export and erasure share the same subject boundary#
- The
continuity_stateexport collector includes the exact user's messages, suppressions, delayed dispatches, and revision as acustomer_message_centerpartition. An absent or fenced subject contributes no row. - Account-level continuity deletion composes
deleteCustomerMessageCenterForSubject. The serialized candidate deletes the exact user's messages, suppressions, and delayed work, records a durable fence, preserves adjacent ids, and rejects stale sends or schedules after restart. - Deletion persistence failure leaves both data and fence unpublished. This prevents a false deletion receipt and avoids resurrecting a send that was already ordered before the successful erase.
- Profile channel Remove is not that account-level erasure. It only changes the binding row and does not delete message-center history or suppressions.
Boundaries and gaps#
- A BFF dispatch is not provider delivery. The route's “delivery seam” ends at an acknowledged durable inbox mutation.
- A channel label is not a channel adapter.
email,sms,push, andvoiceclassify rows and policies; they do not invoke those transports. - A
queuedmarker is not provider delivery. It proves acceptance by the local engine only. - A replay-stamped
deliveredmarker is not a provider callback. The same BFF method authors both later receipt stages without external confirmation. - A durable message snapshot is not an automatic replay worker. Delayed jobs survive restart and failures, but only an explicit admin request scans them; there is no timer, distributed claim, or leader lease.
- One durable snapshot is not a multi-instance transaction log. The candidate-before-publish design protects acknowledged state and restart recovery, but the snapshot interface does not itself prove concurrent writer arbitration across multiple BFF instances.
- An exact-subject eraser is not profile-channel removal. Account deletion clears and fences message-center continuity; removing one email/SMS/WhatsApp binding invokes no message-center eraser.
- A notification projection is not a second store. Inbox and drawer agree
precisely because both read and mutate
customerMessageCenterStore. - A customer-message badge is not a combined drawer aggregate. Domain-feed items can appear in the drawer without contributing to the successful message unread override.
- A browser snooze is not a server schedule. It lives only in component state, disappears on reload, and affects neither dispatch nor delayed replay.
- An accepted opt-out is not hydrated preference UI. The BFF policy can be active while a reloaded page presents the default topic state.
- A fixture receipt is not live delivery evidence. Twelve browser cases intentionally trade integrated authority for deterministic UI branches.
- Deep coverage is not external delivery proof. Provider, OS permission, cold-start notification click, upstream event production, automatic replay, and multi-instance behavior remain outside the grade.
- Source reconciliation is not execution. The focused unit durability suite was rerun at 11 passed and the database-gated case at 1 skipped without its URL; neither cited Playwright application suite was rerun for this documentation audit.
Re-run evidence#
Run the real-BFF and browser specs sequentially against one immutable commit and retain their separate reports. Run the unit durability suite on every build and the Postgres-gated case when its database URL is available:
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/messages-center-bff.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/messages-center.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/bff exec vitest run src/customer/customer-message-center-store-durability.spec.ts
OSHUN_ADMIN_DATABASE_URL=postgres://… pnpm --dir apps/oshun/bff exec vitest run -c vitest.integration.config.ts src/customer/customer-message-center-durability.integration.spec.ts
Retain the commit, BFF/web logs, generated customer and admin identities, message ids, snapshot-store tier, failure-injection output, before/after restart snapshots, deletion/export read-back, and database-gate status. A production-grade follow-up should additionally retain multi-instance results, the automatic scheduler trace, provider request/callback ids, bounce/open evidence, audit events, and OS notification-click reports.
Source trail#
- Current journey contract
- Current coverage registry
- Messages page component
- Same-origin inbox proxy
- Customer message routes and policy gates
- Durable BFF store, delayed queue, and erasure fence
- Message-center durability unit suite
- Gated real-Postgres continuity suite
- Message, receipt, and suppression engine
- Notification projection
- Notification write-back
- Continuity export collector
- Account-level continuity erasure composition
- Shell badge and drawer host
- Live and fixture browser spec
- Real-HTTP BFF spec
Cross-references#
- Messages view evidence
- Shell notification view evidence
- Quiet hours and channel binding journey
- Quiet hours and channel binding result
- Generic PWA behavior evidence
- Current Docs Center coverage audit
Open questions#
- Which cross-instance concurrency contract will prevent two BFF writers from replacing one another's snapshot while preserving the current candidate-before-publish semantics?
- Which scheduler will claim due jobs safely, retry with idempotency, recheck crisis policy, and acknowledge only after the durable transition already implemented by replay?
- Which provider adapters and callback contracts will supply genuine message ids, accepted/delivered/bounced/opened stages, and secret-safe evidence?
- Should the shell badge aggregate customer messages and domain-feed rows, or label its customer-message-only count explicitly?
- How will
/messageshydrate active subscription policies, reconcile the required-category guard across both controls and the server, and represent voice without collapsing it to webhook? - Which cursor contract will expose message history beyond the newest 25 while preserving full aggregate counts and stable ordering?
- Which audit taxonomy records dispatch policy, replay, follow, read, archive, unsubscribe, and re-subscribe with actor, user, tenant, outcome, and reason?