V1 Web PWA · Walk result

Journey result: Quiet hours, verified channels, and provider boundaries

A dated evidence record for V1 Web PWA: observed behavior, current source reconciliation, automation evidence, and explicit proof limits.

verdict: partial· 2026-05-29
8sections19 minread1table

On this page
  • Walked: 2026-05-29 at bf12b0f7d8c291499e1df1ec75d4bd21acf3f6b1. The retained result reported a passing route walk through an ephemeral /tmp script; it did not preserve the script, raw report, browser trace, or provider evidence.
  • Reconciled: 2026-07-18 against the current read-only summary, mutable profile controls, preference stores, channel-binding lifecycle and snapshot wire, durable message-center delay/replay path, Telegram HMAC routes and user-state snapshot, reminder worker, provider router, notification, reminder, message-center, and Telegram write-through/erasure suites, and all seven named Playwright specs. The message-center, channel-binding, and Telegram unit suites passed freshly at eleven, 21, and six cases; their real-Postgres cases each collected as one skip without a database URL. This was not a fresh application run.
  • Verdict: partial / deep for the shipped control planes — the current evidence deeply covers summary-to-editor handoff, quiet-hours and domain preference round trips, a non-production email/SMS/WhatsApp verification lifecycle, durable custom notification preferences, durable reminder schedule/inbox/erasure state, required restart-safe channel bindings, durable message-center delay and manual replay, safety bypass, exact profile/session/ message-continuity erasure and export, and restart-safe Telegram link, capture, preference, suppression, export, and erasure state. It does not prove production code delivery, an atomic master/window update, automatic message-center replay, genuine account step-up, identity convergence, or delivery to a profile-bound email/SMS/WhatsApp address.
  • Current authority: WALKTHROUGH/journeys/messages-quiet-hours-and-channel-binding.md defines the same split authorities and partial outcome.

Result at a glance#

Interaction Proof · limit
Read-only summary and handoff Proof: ten browser cases cover the account-scoped four-channel, five-category projection, preference read-back, explicit failure copy, editor link, anonymous redirect, footer behavior, 390 px stacking, standalone mode, and real-service-worker replay. Limit: the page is a static projection; its server fetch collapses an unavailable or rejected response into an empty state.
Quiet-hours and domain editor Proof: five browser cases and twelve real-HTTP cases cover the natural two-write enable flow, start/end time, timezone, normalized weekday mask, digest cadence, all six domain matrices, auth, scope, and validation. The custom preference store now commits through a required database-backed snapshot. Limit: the consumer-profile master and custom window remain separate durable records written by separate requests, so partial success can still make their effective values drift.
Email/SMS/WhatsApp binding Proof: two browser cases drive real BFF request, salted-hash pending state, preview-code verification, wrong-code persistence, secret-free reload, typed removal, 390 px controls, and service-worker behavior. Production requires the snapshot; request, verify, and unbind await candidate persistence. A 21-case unit suite proves verified/pending restart, rollback, race ordering, and erasure fences. Limit: only non-production exposes a usable code, and the real-Postgres restart case is environment-gated.
Verification-code delivery Proof: the production branch honestly returns delivery: missing-config and omits the plaintext code. Limit: no email, SMS, or WhatsApp transport is invoked by the bind route, so a production user cannot complete the lifecycle from this surface.
Bound-address delivery Proof: current source maps verified binding fields into a reminder recipient, gates matching reminder channels on verified status, and awaits a durable reminder snapshot before acknowledging production schedules or cycles. Limit: automatic producers all choose push, so their copied email/SMS/WhatsApp fields are ignored; manually scheduled external reminders carry an independently supplied recipient. No named suite proves provider delivery to the stored profile address.
Quiet-hours dispatch Proof: the sixteen-case BFF suite covers non-essential delay, due replay, crisis-frame hold, incident bypass, and resulting inbox receipts over real HTTP. The required message-center snapshot preserves delayed work, inbox rows, receipts, and holds; failed replay persistence keeps work queued. Limit: replay is an explicit admin call without a timer or claim lease, and its sent/delivered stages are authored by the BFF rather than provider callbacks.
Telegram identity seam Proof: five real-BFF Login Widget/step-up cases verify a valid HMAC, two invalid HMAC forms, the non-sensitive scope set, and action classification; two of the 24 webhook-suite cases add Mini App HMAC rejection. A six-case unit suite proves write-before-publish link/capture state, rollback, restart, ordered erasure fences, and fail-closed production use; its one-case Postgres companion is environment-gated. Limit: the public Login Widget still mints an iris:<telegram-id> subject rather than one of the three profile rows, so this does not prove convergence with a signed-in profile or bot-provider delivery.
Removal Proof: a verified row rejects DELETE without the literal channel-revoke header value, then awaits a durable unbound projection. Account deletion durably erases/fences the exact profile channel bindings, Telegram profile/captures, custom preferences, device sessions/tokens, notification read state, reminder continuity, and message-center partition; profile, saved-content, and session exports include the corresponding records. Limit: the phrase is client-known confirmation, not primary-credential step-up, and removing one profile channel invokes none of the other erasers or provider, Telegram, device-token, or chat cleanup.

Evidence map#

This vertical authority ledger keeps three independent planes separate. Solid edges are exercised by the named application suites; dashed edges mark the production meaning still missing. The stacked lanes are reading order, not a claim that profile binding, message-center replay, and Telegram call one another.

flowchart TB A[Profile summary and mutable editor] --> B[Durable binding snapshot plus preview verification] B -. missing .-> C[Production code and bound-address delivery] C ~~~ D[Separate durable master and preference snapshots] D --> E[Message policy, durable delay, and admin replay] E -. missing .-> F[Message-center timer, claim lease, and provider callback] F ~~~ G[Durable Telegram HMAC identity and user state] G -. separate .-> H[Profile binding inventory]

Proven observations#

The historical pass remains a dated route observation, not the current grade#

  • The old result observed /profile/notifications, four channels, four categories, and no feature-flag rate limit. The current BFF projection has four channels and five categories after adding the Arete weekly-review row, so the historical count is no longer a product contract.
  • The current ten-case index spec proves more than the old walker: account-email read-back, master push reflection, Arete reminder reflection, fail-closed empty copy, editor navigation, anonymous redirect, touch-safe footer targets, phone stacking, standalone mode, and an actual service-worker document replay.
  • That suite still does not recover the historical raw walk or rerun the feature-flag premise. The old zero-error and zero-failure statements therefore remain dated observations at the walked commit.

The summary and editor expose two different jobs#

  • /profile/notifications is a server-rendered, read-only projection of channels and categories. “Change delivery settings” opens /profile?path=notifications, where the notification matrix and quiet-hours controls live. The email/SMS/WhatsApp binding rows live in the broader profile settings panel, not in /messages and not as switches on the summary page.
  • The summary's four channels are email, push, SMS, and Telegram. They are not the same inventory as GET /v1/profile/channels, whose bindable set is exactly email, SMS, and WhatsApp. A summary label therefore cannot be read as a verified binding or transport capability.
  • A null BFF read becomes zero channels and zero categories with explicit recovery copy. That is honest about the unavailable projection, but it does not distinguish authentication rejection, network failure, malformed data, and a legitimately empty account.

The profile binding lifecycle is real, secret-safe, and non-production-only#

  • The bind route is user-scoped and abuse-protected. It accepts only email, SMS, or WhatsApp, generates a six-digit code, stores a salted hash with TTL and attempt state, and returns a secret-free binding projection. The plaintext code is not persisted.
  • Non-production returns delivery: preview and the code. Production returns delivery: missing-config with no code. The route does not call the real provider router, even when email/Twilio/Meta credentials exist elsewhere in the process.
  • Verify distinguishes verified, wrong-code, expired, locked, and not-pending as 200 outcomes for an existing row; only an absent binding is a 404. The UI can therefore preserve a pending row and explain an incorrect code instead of collapsing it into a generic request error.
  • The two-case browser suite performs the complete preview-code email lifecycle against real HTTP, reloads the full route after verification and removal, proves a wrong SMS code remains pending, and checks every affected control at a 390 px viewport for 44 px targets and zero page overflow. It does not stop and restart the BFF.
  • Deployable startup calls requireDurableChannelBindings and hydrates the snapshot before traffic. Bind, confirm, and unbind clone the last committed state, serialize mutation, persist, then publish; persistence failure maps to 503, an erased subject to 409, and neither a new address nor a burned verification attempt leaks into the visible view.
  • The 21-case focused store suite proves authoritative-empty boot, verified and pending-code restart recovery, user isolation, failed-write rollback, exact-subject erasure, adjacent-user preservation, both stale-mutation race orders, failed-deletion rollback, and fail-closed missing-sink behavior. Its one-case real-Postgres companion repeats verified-state restart and a signed profile erasure fence, but collected as skipped here without a database URL.
  • The profile export includes the secret-safe binding record, revision, and statuses. Account-level profile deletion composes the binding eraser; a successful fence rejects stale requests after restart without deleting an adjacent subject.

Quiet hours span two durable snapshots and a non-atomic editor#

  • The natural UI toggle performs two writes. /v1/preferences updates the consumer profile's durable quietHoursEnabled master flag; a separate PATCH /v1/notifications/preferences durably updates the window. Time, timezone, weekday mask, digest settings, six domain records, and persona records share a second database-backed snapshot with per-subject revisions and deletion fences.
  • GET and PATCH responses overlay the consumer profile's master flag onto the preference snapshot's quiet-hours object. The reminder worker does the same. The message-center dispatch route, however, reads the preference object's own enabled field directly. Both authorities survive restart, but the editor's sequential requests have no cross-store transaction or compensating rollback; a failed second write or direct single-route caller can expose different effective state.
  • The twelve-case API suite proves same-process PATCH→GET round trips for the custom window and every domain × modality × kind record, including scope and validation gates. Its first direct PATCH deliberately asserts only that enabled is a boolean because the response is sourced from the separate consumer profile. The named browser/API suites are not restart tests.
  • The five-case browser suite proves the natural dual-write toggle, a narrowed weekday mask and changed start time after reload, global push gating, Nisaba/Metis lanes, and digest recommendation copy. Reload exercises the same BFF process.
  • Focused notification durability tests now prove write-before-publish rollback, ordered retry, restart restoration, exact-subject erasure, adjacent-subject preservation, and stale-write rejection for both custom preferences and read state. A gated real-Postgres suite repeats restart and erasure-fence proof.

The message-center proof is a policy seam, not bound-channel transport#

  • messages-center-bff.spec.ts has sixteen real-HTTP cases. Four are directly decisive here: a routine message inside the active window is delayed without an inbox row; explicit after-window replay writes the row and later receipt stages; an active crisis frame holds due replay; and a critical incident bypasses delay.
  • Quiet-hours delayed jobs share the required message-center snapshot with messages, receipts, suppressions, revisions, and deletion fences. Production startup hydrates it before traffic, and dispatch awaits persistence before returning 202; no timer claims due work, so an admin must still invoke POST /v1/admin/customer-messages/replay-due.
  • Replay commits a successful message, BFF-authored receipts, and delayed-row removal as one candidate. Snapshot failure retains the original job; policy rejection or a gate exception becomes a durable queued failure/hold. The focused eleven-case unit suite also proves restart recovery, write rollback, exact-subject erasure, adjacent-user preservation, and send/delete ordering; one database-gated case repeats restart and erasure-fence proof on Postgres.
  • This dispatch route does not read GET /v1/profile/channels and never invokes email, SMS, WhatsApp, push, or Telegram providers. Its channel value selects local policy and receipt labels only. Replay authors sent and delivered itself without receiving a provider message id or callback.

Real reminder transports exist, but the profile-bound handoff is incomplete#

  • The separate reminder cycle can call real SendGrid/SMTP, Twilio, Meta WhatsApp, FCM, APNs, Expo, or Web Push transports when matching credentials and recipient fields are present; missing credentials yield missing-config, never fabricated success. In-app reminder delivery is available independently.
  • BFF startup resolves verified email/SMS/WhatsApp binding fields into each automatic producer's MessageRecipient, and reminder policy suppresses an explicitly scheduled email/SMS/WhatsApp channel without a verified binding. That is meaningful source wiring, but it does not complete this journey.
  • Reminder schedules, delivered-id ownership, the in-app inbox, and deleted-recipient fences now share a database-backed snapshot. Production requires the sink; schedule, cycle, and erasure mutations serialize, commit the candidate snapshot before publishing it in memory, and return explicit persistence or deleted-subject failures instead of acknowledging drift.
  • Focused durability tests cover failed commits, retry, exact-subject deletion, adjacent-subject preservation, in-flight create/delete ordering, legacy-owner repair, restart hydration, and a gated real-Postgres restart/erasure path. The optional worker now acquires a cycle lease before producing or delivering: shared Redis provides the cross-replica fence, while a single-process deployment uses an explicit local lease. Lease failure skips the tick without consuming scheduled work.
  • The streak, Nyx, due-assignment, upcoming-session, and content-drop automatic producers all set channel: 'push'. The delivery router therefore consults a push token and ignores the copied email, phone, and WhatsApp recipient fields. Enabling the optional worker does not by itself send to those bound addresses.
  • The admin schedule route can submit an email/SMS/WhatsApp reminder, but its payload supplies the recipient separately. Awaited snapshot persistence makes the schedule durable; the policy check still verifies only that some binding exists for the channel, not that the supplied address equals the verified address. No named suite demonstrates or closes this join.

Telegram is a different identity system#

  • The five-case Telegram binding suite signs a genuine Login Widget payload, proves the exact non-sensitive scopes, rejects tampered and missing hashes, and classifies six sensitive actions as requiring primary-credential challenge. The Login Widget now awaits a durable Telegram user-state binding; the authenticated link-token flow, webhook completion, /unlink, captures, voice preference, and delivery suppression use the same ordered snapshot.
  • Production startup requires that snapshot and hydrates it before traffic. The six-case focused suite proves candidate-before-publish capture and link writes, failed-write rollback, restart restoration, independent profile and saved-content fences, adjacent-subject preservation, both write/delete race orders, failed-erasure rollback, and fail-closed missing-sink behavior. Its one-case real-Postgres companion repeats restart, export, and both erasure fences, but collected as skipped here without a database URL.
  • The profile export includes secret-safe link, voice, and suppression state; raw link nonces are hashed. The saved-content export includes Telegram captures. Account deletion composes the profile and capture erasers as two independent signed-service categories.
  • The public Login Widget route derives oshunUserId as iris:<telegram-user-id>; it does not authenticate a current profile member or create an email/SMS/WhatsApp row. The authenticated deep-link account flow can bind a profile subject, but the five browser cases do not prove that convergence or a profile-inventory projection. Bot reply delivery remains a distinct provider path.
  • telegram-webhook-delivery.spec.ts contains 24 cases, but only its final two Mini App cases directly support this binding result, and both are negative HMAC branches. The other 22 belong to the Telegram assistant-delivery journey and must not inflate this page's ownership proof.

Boundaries and gaps#

  • A durable binding is not a delivered verification code. The required row and its salted code hash survive restart while production still returns missing-config.
  • A preview code is not production verification. It exists only outside production so development and browser automation can finish the round trip.
  • A verified address is not provider consent. The profile flow records address ownership, not tenant permission, A2P/WhatsApp opt-in, capability, residency, suppression, or provider receipt.
  • Recipient resolution is not bound-address delivery. Automatic producers choose push, and manual schedules supply their own recipient object.
  • A durable reminder snapshot is not a bound-recipient join. It preserves exactly the recipient object the producer or admin supplied; it does not prove equality with the profile binding.
  • A cycle lease is not a bound-recipient join. It prevents concurrent reminder cycles; it does not prove that a supplied delivery address matches the profile's verified binding.
  • Two durable snapshots are not one atomic quiet-hours commit. The master flag and custom window persist independently, while the editor issues two requests and neither store rolls the other back after partial failure.
  • A named browser reload is not the retained restart proof. The source-level notification, binding, and Telegram durability suites cover restart and erasure, while the seven named Playwright specs keep one BFF process and do not retain a backing-tier restart trace.
  • A durable delayed entry is not an automatic replay scheduler. The job and its failure state survive restart, but they move only after an explicit admin call; there is no timer, distributed claim, or leader lease.
  • A database-backed snapshot is not multi-writer arbitration. Ordered candidate-before-publish mutation protects one process and acknowledged restart state, but does not itself prove concurrent BFF writers cannot replace one another's snapshot.
  • A replay-authored receipt is not provider acknowledgment. The same BFF writes the queued, sent, and delivered stages without a provider callback.
  • A static phrase is not account step-up. channel-revoke is shipped in the client and compared literally by the server.
  • An unbound row is not a revocation cascade. Account-level data deletion can erase and fence durable profile bindings, device sessions/tokens, notification preferences/read state, reminder continuity, and exact-subject message-center history/suppressions/delayed work, plus the exact-subject Telegram profile and capture partitions. The single profile-channel DELETE does not invoke those other erasers; independently supplied recipients, provider subscriptions, other bindings, device tokens, Telegram state, Mini App sessions, and chat caches are not cancelled by unbinding.
  • A Telegram-derived Iris subject is not a profile channel binding. It is a separate HMAC-backed identity seam with durable link and unlink paths. Exact-subject export and erasure do not make iris:<telegram-id> the same subject as an independently authenticated profile member.
  • Broad suite membership is not direct proof. Only two of 24 Telegram webhook cases concern the Mini App HMAC boundary on this page.
  • Deep control coverage is not a complete delivery journey. Production code transport, atomic master/window updates, safe scheduler ownership, profile-bound provider delivery, callbacks, consent, and revocation remain outside the grade.
  • Source reconciliation is not execution. The focused message-center and channel-binding unit suites were rerun at 11 passed and 21 passed; the Telegram durability suite passed 6 cases. Their database-gated companions each collected as 1 skipped without a URL. None of the seven cited Playwright application specs was rerun for this documentation audit.

Re-run evidence#

Run the seven focused specs sequentially against one immutable commit and one controlled BFF/web environment:

bash
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/profile-notifications-index.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/profile-notification-preferences.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/notifications-preferences-api.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/profile-channel-bindings.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/telegram-binding-bff.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/telegram-webhook-delivery.spec.ts --project=chromium --workers=1
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/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
pnpm --dir apps/oshun/bff exec vitest run src/messaging-channels/channel-bindings-store.test.ts
OSHUN_ADMIN_DATABASE_URL=postgres://… pnpm --dir apps/oshun/bff exec vitest run -c vitest.integration.config.ts src/messaging-channels/channel-bindings-durability.integration.spec.ts
pnpm --dir apps/oshun/bff exec vitest run src/telegram/user-state-durability.spec.ts
OSHUN_ADMIN_DATABASE_URL=postgres://… pnpm --dir apps/oshun/bff exec vitest run -c vitest.integration.config.ts src/telegram/user-state-durability.integration.spec.ts

Retain the commit, environment mode, storage tier, generated customer and admin subjects, browser/BFF reports, binding and reminder snapshots, preference snapshots before and after process restart, erasure-fence read-back, and delayed message-center job/export state. Record both Postgres-gate outcomes and profile/ saved-content/session export and erasure read-back, including both Telegram partitions. A production-grade follow-up must also retain verification-code provider ids, bound-address equality proof, automatic-worker claim/retry evidence, provider request/callback ids, consent and suppression read-back, genuine account step-up, and complete revocation audit evidence.

Source trail#

Cross-references#

Open questions#

  • Which provider adapters will deliver binding codes, retain attempt-safe provider ids, and surface retryable versus permanent failures without ever returning a plaintext production code?
  • Which transaction, coordinator, or convergence rule will make the durable master flag and durable window/domain/persona snapshot one atomic customer intent across two routes and stores?
  • Which scheduler will durably claim delayed messages, re-evaluate crisis and preference policy, retry idempotently, and acknowledge only after a durable transition?
  • Which leader-election or claim protocol will make the durable reminder worker safe across multiple BFF instances rather than one designated process?
  • Should automatic reminder producers select a verified email/SMS/WhatsApp binding when push is unavailable, and how will tenant allowlists, explicit opt-ins, cost, residency, and fallback order govern that choice?
  • How will manual schedules prove their recipient equals the verified binding rather than accepting an unrelated address for the same channel?
  • Which primary-credential challenge will replace the public revoke phrase, and which services must participate in a verifiable revocation cascade?
  • Should Telegram Login Widget identity, authenticated account deep-linking, Mini App sessions, bot suppression, and profile channel inventory converge on one explicit account-binding model or remain separate products?
  • Which retained provider and restart run will promote this result from deep control-plane coverage to an end-to-end delivery grade?