---
status: walked + e2e-covered
coverage_depth: deep-for-durable-bff-core
last_walked: 2026-07-02
last_reconciled: 2026-07-18
specs:
  - apps/oshun/web/e2e/messages-center.spec.ts
  - apps/oshun/web/e2e/messages-center-bff.spec.ts
  - apps/oshun/bff/src/customer/customer-message-center-store-durability.spec.ts
  - apps/oshun/bff/src/customer/customer-message-center-durability.integration.spec.ts
source: WALKTHROUGH/journeys/messages-center-deliver-and-unsubscribe.md
---

# Journey result: Messages center dispatch, follow, and subscription control

- **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.md`](../journeys/messages-center-deliver-and-unsubscribe.md)
  starts 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.

```mermaid
flowchart TB
    A[Admin-scoped dispatch] --> B{Crisis and quiet-hours policy}
    B --> C[Outcomes: send, durable delay, or suppress with no row]
    C -->|send or delay| D[Required snapshot: inbox, receipts, opt-outs, delayed work]
    D -->|delayed only| E[Explicit admin replay-due call]
    D -->|send| F[Messages page plus notification projection]
    E --> F
    F --> G[Durable follow, read, archive, or opt out]
    G ~~~ H[Continuity export plus exact-subject erasure]
    H ~~~ I[Missing: automatic claim lease and provider callback]
```

## 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_frame` and writes no message. A
  critical incident bypasses both crisis suppression and a current quiet-hours
  window with `dispatch.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 `queued` marker. The route awaits the snapshot
  before returning `201`, 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 `requireDurableCustomerMessageCenter` before 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 `/messages` through the natural
  authenticated Next proxy. The producer seed uses a test-generated
  `admin:messaging` development 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 `clicked`
  read-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 `sendMessage` always begins with `queued` and notes `enqueued`. 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 `sent` and `delivered` with 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 set `readAtUnixSeconds`; only the explicit read
  action does.
- The browser projection maps clicked and opened stages to one `opened` display
  state and copies free-form receipt notes into a `providerMessageId` model
  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 to `503` and an erased subject to `409`.
- `/v1/notifications` reads the same store's newest 25 unarchived rows, maps
  each to `customer-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_read` awaits `markReadDurably`; dismiss awaits
  `archiveDurably`; 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,
  `customerMessageUnreadCount` replaces the supplied general notification count.
  The observed `2 → 1 → 0` convergence 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 `topics` array 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.
- `voice` exists in the server channel contract, but the shell display model has
  no voice channel and maps it to `webhook`. 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_state` export collector includes the exact user's messages,
  suppressions, delayed dispatches, and revision as a `customer_message_center`
  partition. 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`, and
  `voice` classify rows and policies; they do not invoke those transports.
- **A `queued` marker is not provider delivery.** It proves acceptance by the
  local engine only.
- **A replay-stamped `delivered` marker 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:

```bash
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](../journeys/messages-center-deliver-and-unsubscribe.md)
- [Current coverage registry](../journeys/coverage.md)
- [Messages page component](../../apps/oshun/web/src/components/MessageCenter.tsx)
- [Same-origin inbox proxy](../../apps/oshun/web/src/app/api/messages/inbox/route.ts)
- [Customer message routes and policy gates](../../apps/oshun/bff/src/routes/customer-message-center.ts)
- [Durable BFF store, delayed queue, and erasure fence](../../apps/oshun/bff/src/customer/customer-message-center-store.ts)
- [Message-center durability unit suite](../../apps/oshun/bff/src/customer/customer-message-center-store-durability.spec.ts)
- [Gated real-Postgres continuity suite](../../apps/oshun/bff/src/customer/customer-message-center-durability.integration.spec.ts)
- [Message, receipt, and suppression engine](../../libs/oshun/customer-message-center/src/message-center.ts)
- [Notification projection](../../apps/oshun/bff/src/routes/notifications.ts)
- [Notification write-back](../../apps/oshun/bff/src/routes/notifications-write.ts)
- [Continuity export collector](../../apps/oshun/bff/src/data-export/bundle-builder.ts)
- [Account-level continuity erasure composition](../../apps/oshun/bff/src/data-deletion/continuity-state-eraser.ts)
- [Shell badge and drawer host](../../apps/oshun/web/src/components/ShellLayout.tsx)
- [Live and fixture browser spec](../../apps/oshun/web/e2e/messages-center.spec.ts)
- [Real-HTTP BFF spec](../../apps/oshun/web/e2e/messages-center-bff.spec.ts)

## Cross-references

- [Messages view evidence](../customer/02-home-discovery/messages.md)
- [Shell notification view evidence](../shell/05-notifications.md)
- [Quiet hours and channel binding journey](../journeys/messages-quiet-hours-and-channel-binding.md)
- [Quiet hours and channel binding result](./messages-quiet-hours-and-channel-binding.md)
- [Generic PWA behavior evidence](../shell/03-pwa-behavior.md)
- [Current Docs Center coverage audit](../../DOCS_CENTER_PAGE_AUDIT_2026-07-16.md)

## 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 `/messages` hydrate 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?
