Consumer product
Consumer release checklist
One ordered checklist for new, existing, guest, offline and cross-device users across iOS, Android and the current consumer web. Capture recording stays mobile-only; web views the resulting encounters.
For now, staging and production web must stay aligned. Mobile staging remains a separate test release until dedicated App Store and Play Store production builds launch.
Supabase is the current backend. A future AWS or other-provider move remains an open production architecture decision, so current work preserves clear API and data-migration boundaries without delaying this release.
Updated 26 August 2026
Verified complete
29 of 59
49%
A checkbox is marked only after the required deployment and device or browser evidence exists, not merely when code has been written.
Stabilise the current foundation
5/6Put the hardened foundation into one compatible staging environment before expanding the audit.
Push the current consumer hardening batch
True for that specific batch - local staging is 0/0 ahead-behind origin/staging as of this check. Read narrowly: everything from Phases 1-10 of this audit (the stable-refresh pattern, all notification/auth/scanning/event/card fixes, 3 new migrations, ~9 new test files) is still uncommitted working-tree state, not part of "the pushed batch." Not a gap in this item's own claim, but worth not misreading as "everything from this session is pushed."
DoneDeploy the atomic Capture database migration
Deployed to staging from d6518a0; workflow 32966016162 passed and the RPC availability probe succeeded. The 3 newer migrations from this session (notification preference gaps, shared-encounter link expiry, assistant rate limiting) shipped in the same commit as their dependent application code, then deployed to both staging (workflow 33013624732) and production (workflow 33013876815) on 26 August 2026 - both passed, so no code-ahead-of-migration window ever existed for these.
DoneVerify matching staging and production web deployments
Production fast-forwarded from 73743c2 to d6b2caa (staging's HEAD) on 26 August 2026 - a clean fast-forward with zero commits unique to main. Both branches point at the same commit; the deploy-migrations and security-safeguards workflows both ran on the push to main and passed.
DoneRun staging smoke tests on iOS, Android and web
Authenticated staging web passed Home, Connections, Follow-ups, Scan and My Account with no browser errors. iOS 26.5 Simulator passed signed-in Home, My Cards, My Account and the Assistant open / voice-to-keyboard / New regression path. The old iPhone build stayed alive across core route launches. On a physical Galaxy S9, Android staging-store 1.1.1 (10) passed signed-in Home, My Cards, My Account, Quick Actions, Assistant starter, live recognition and voice-to-keyboard without fatal logs; nav icons remained visible. Both devices still need the compatible replacement build, complete actions, exact deep links and sustained soak before release certification.
In progressKeep live screens stable during background refresh
Home, People, Cards, Follow-ups, Events, Notifications and Capture now coalesce focus/timer/manual loads, preserve the last successful snapshot, reserve skeletons for first load, and keep background network failures from blanking or blocking the screen across web and mobile. Cross-checked for setInterval-driven fetch surfaces outside that original list: found and fixed one real gap - Capture's 5-second inbound-exchange poll blunt-replaced its list every tick and, on a transient network error, wiped an already-loaded list back to empty (worse than the flash/reflow problem this item otherwise fixes). Also applied keepStableJson to Events' background poll, which already had single-flight coalescing but still re-rendered its list/candidates on every tick even with no changes.
DoneCreate compatible staging native builds before another OTA
Built and submitted aftermeet-staging iOS build 16 and Android build 11 (TestFlight and Play internal testing), recorded them as the new runtime-baseline.json fingerprint, confirmed check-runtime-safety.mjs passes against current source, and published a real OTA update against them successfully. Physical-device confirmation that these specific builds behave correctly on-device is tracked separately under the staging smoke-test item above, not blocking this one.
Done
AI assistant
4/6Make every conversation and action dependable across supported mobile devices.
Conversation storage, reopening and deletion
DoneWorkspace-scoped context and memory boundaries
DoneCapture, Follow-up and Card action receipts
Cross-checked beyond the original claims: event proposals inherit the same offline/error handling as manual event creation (no assistant-specific gap) and the streaming reply path already recovers cleanly from a dropped connection. Fixed: a card-draft proposal is now marked done the moment createCard() actually succeeds, not after the receipt write - a receipt-write failure used to fall through without marking the proposal done, leaving "Create draft" tappable again and able to create a second, duplicate draft from the same proposal on retry.
DoneVoice, keyboard and old-device compatibility matrix
iOS 26.5 Simulator passed assistant open, New, visible text entry, submit and voice/text transition without a crash. Both native speech import and proxy invocation now fail closed. The old iPhone binary stayed alive on Assistant launch. A physical Galaxy S9 running staging-store 1.1.1 (10) passed signed-in Assistant starter, live recognition, recognition stop, keyboard composer positioning and voice-to-keyboard with no fatal or missing-ExpoSpeech log. Conversation continuation/New and the replacement current-source builds remain pending.
In progressAll action destinations and failure recovery
All nine guided destinations are mapped to real mobile screens; recording storage opens its exact sheet, completed Follow-up/Card/Event/Capture writes retain safe internal receipts, and created follow-ups reopen the exact saved item. Fixed: /api/assistant/query had no request-frequency limit at all despite running at least two paid AI-provider calls per request - now rate-limited server-side (20/minute, 120/hour per user) on top of the existing input-size bounds.
DoneLong-session and physical-device stress test
The paired iPhone cold-launched without a new crash, but its old development binary has a historical 89%-CPU / 1.35-GB resource report. Re-run a sustained foreground/background Assistant session on the replacement release-signed iOS and Android builds and require a clean device/Sentry window.
In progress
Notifications and Inbox
4/5Audit every producer, preference, badge, destination and actionable notification as one system.
Event, nudge and Reach Out routing
DoneInventory every notification type and producer
All 13 canonical types now have matching server/mobile unions and preference rows; every producer, preference gate, dedupe key, action identity and push/in-app route was traced.
DoneUnread, badges, mark-all-read and deduplication
Owner-scoped read updates, mark-all-read, actionable-nudge semantics and unique dedupe keys are code-verified. Mobile read mutations now persist in a scoped retry queue instead of reverting after an offline tap. Cross-checked all 13 canonical types' preference gates individually - the dead-toggle bug class already found and fixed 3 times does not recur anywhere else; fully closed. Fixed: the OS app-icon badge was derived from open-follow-up count, not unread notification count - a user with unread notifications and zero open follow-ups saw badge 0, and reading a notification never cleared a badge driven by an unrelated number. Fixed: connection_added push fired for every active workspace member on every mutual-connect, even a repeat between the same two workspaces where the in-app row was silently skipped by SQL's dedupe constraint - push now only targets recipients who actually got a new row.
DoneExact mobile and web destinations
Follow-up pushes and inbox rows reopen the exact action; web notification taps now route contact requests, connections, nudges and follow-ups instead of silently marking them read or opening an unrelated meeting. Fixed: the web bell popover only ever showed its 8 most recent notifications with no other way to reach anything past that, even though the unread badge and mark-all-read already covered the hidden ones. Added a full notification-history page matching mobile's parity.
DoneOffline, revoked and cross-device notification states
Mobile now retains a scoped 100-row offline snapshot and retries pending read mutations on authenticated refresh. Physical terminated-app, delayed-push, two-device convergence and revoked-target evidence remains required.
In progress
Authentication and onboarding
2/5Give new, existing, converted and returning users the same correct starting state.
Email and Google sign-in matrix
All web sign-in methods now preserve card, exchange, event and shared-meeting intent. Physical new/existing-account evidence remains required.
In progressSession expiry, recovery and account switching
Mobile post-auth navigation now waits for bounded workspace provisioning and fails closed instead of admitting an unprovisioned session. Cross-checked account switching specifically for cached-data leakage between accounts on the same device (scoped storage, purged on user-id change, gated first-render) - no leak found, already solid. Fixed: a confirmed-dead session (refresh token also rejected) had its HTTP status discarded by most of mobileFetch's 19+ callers, so a user with an expired session saw a generic "Could not load your X" instead of a sign-in prompt - mobileFetch now throws a typed, friendly SessionExpiredError once, centrally, so every caller gets the accurate message without its own status check. Expiry and two-account physical evidence remains.
In progressVisitor conversion without duplicate identity
Email code, Google One Tap and OAuth now retain exchange, card, event and shared-meeting linking through completed and interrupted onboarding paths.
DoneFirst card and workspace assignment
Provisioning is idempotent across old and new accounts, repairs stale active-workspace pointers, assigns card ownership and guarantees one primary consumer card.
DoneCross-device continuity for old and new accounts
Code-checked: all account-defining state is server-side and keyed by user id, not device - provisioning is advisory-locked and idempotent, onboarding status is read fresh from the server on every sign-in, and no local "first launch" flag stands in for or re-triggers first-time account setup. A second device signing in re-runs the same idempotent provisioning and converges on the same workspace and cards; no duplication path found. Physical two-device evidence remains, which is the entire reason this stays in-progress rather than done.
In progress
Scanning and connection exchange
2/5Certify the complete two-person QR and connection loop on real devices.
Atomic, rate-limited public exchange
DoneConnection ownership and duplicate protection
DoneCamera, invalid card and revoked card states
Invalid-QR parsing and cross-environment slugs were already rejected before any network call. Fixed: a scanned card that is archived, unpublished, or was never valid in this environment now returns the same specific explanation regardless of which lookup path catches it, instead of a generic "could not be found" on the common path. Fixed: a camera that fails to initialize (no hardware, held by another app) now shows a recoverable error screen instead of a black rectangle. Needs physical-device confirmation of the camera-failure and revoked-card copy.
In progressOffline QR and retry behavior
Code-verified: scans queue to a deduped local store while offline, drain automatically on reconnect via the same foreground-sync pattern as follow-ups, and the queue's permanent-vs-transient distinction already keys off the server's machine-readable code rather than message text. Needs physical offline/reconnect device evidence.
In progressTwo-account physical-device exchange
Not started
Connected accounts and contact requests
2/5Make imported information and missing-detail recovery reliable without weakening privacy.
Contact-request ownership and deduplication
DoneGoogle token lifecycle
Scope: Google only for this release - Microsoft/Outlook connected-account support is deferred (coming soon), not required here. Refresh-on-expiry and reconnect signaling were already solid for calendar sync. Fixed: sendGoogleEmail and createGoogleCalendarEvent now turn a token dying mid-request into the same typed reconnect error the pre-flight check gives, instead of a generic 500 (the Microsoft-provider functions were hardened identically since the code path is shared, but Microsoft itself is not being certified this pass). Client-side one-tap reconnect and physical revoked-token evidence remain.
In progressCalendar import recovery and mobile contact import
Scope: contact import is the existing mobile device-contacts feature, not a Google/Microsoft contacts API - that integration is not planned for this release. Calendar import (Google) per-provider status reporting, conflict preservation, and idempotent upserts were already well built. Fixed: a failed insert of newly-discovered calendar candidates no longer aborts before existing events get their conflict check. Fixed: mobile device-contact import (event invites and capture's add-person picker) now distinguishes a denied Contacts permission from a genuinely empty address book, and tells the person how to fix it, instead of both silently rendering an empty list. Physical permission-denial device evidence remains.
In progressFulfilled request resumes the original action
The return-to-origin OAuth cookie mechanism itself is sound and used correctly elsewhere. Fixed: connecting Google Drive from an in-progress capture draft now returns to that exact draft (with its draftId) instead of always dumping the user on generic settings and losing the recording in progress.
In progressProvider privacy and workspace isolation
Workspace isolation is solid: every connected-account query and RLS policy scopes by workspace_id and user_id together, and no route ever returns a raw token to the client. Fixed: access_token/refresh_token are no longer stored as plaintext - connected_accounts now holds references into Supabase Vault (access_token_id/refresh_token_id), with encrypt/decrypt/update going through security-definer wrapper functions that authorize the same way the table's own RLS policy does (workspace member, active, matching auth.uid()), not by trusting the secret UUID being hard to guess. Table was empty in production at migration time; the migration still backfills any existing plaintext for environments that aren't. Key-management call (Vault over app-level encryption) made by the user.
Done
Account, profile and data controls
2/5Verify that every user can understand, update, export and remove their data safely.
Profile and primary-card photo linkage
DoneNotification and recording-storage settings
Most notification-type toggles were already correctly gated server-side. Fixed: connection_added (in-app row was inserted for every workspace member unconditionally, ignoring the toggle entirely - now checked in SQL and in the push-dispatch path), access_request (was checking notificationTypeEnabled(null, ...), which is always true, instead of the actual recipient's preferences), and access_granted/shared_meeting_update (had no preference check at all). Fixed: cloud recordings could not be reviewed or removed after capture - only the expiry cron ever cleared one. Added a DELETE endpoint and a "Delete recording" action on the meeting screen that removes the cloud copy early without touching the transcript, summary or follow-ups. Physical-device evidence for both remains.
In progressPending sync and local-data controls
Already solid on inspection: mobile's Pending sync screen shows per-category counts, per-item failure reasons, a manual Retry and a Discard action that explicitly preserves unsent recordings; web's Local data settings page shows cache counts with a clear action. No functional gap found.
DoneAccessibility and responsive layout
Targeted static audit, not a full visual/screen-reader pass. Web: image alt text and icon-only button aria-labels are already present throughout Home, Cards, People, Follow-ups and Settings; responsive breakpoints are real and extensive (11 rules on marketing, 25 on the app/settings shell, down to 360px). Mobile: scanned all 232 Pressable elements app-wide; fixed the one missing accessibilityLabel found (the remove-contact-method trash button on a card). Full screen-reader and visual-contrast device pass remains.
In progressFull account deletion and fresh sign-up
Does not exist yet: no delete-account route, RPC, or UI anywhere in the app - confirmed by grep and by this file's own prior status. The schema anticipates it (users.status allows 'deleted', auth.users cascades through users -> workspaces -> workspace_memberships) but no code path ever triggers it, and cascade coverage for cards, encounters, notifications and connected-account tokens has never been exercised. Deliberately not started this pass - it's a materially higher-risk, irreversible build that deserves its own dedicated design and review rather than being bundled into incremental fixes.
Not started
Shared meetings and guests
3/5Prove that public sharing is useful, private and resilient for anonymous and returning guests.
RPC privacy boundary
DoneAtomic guest follow-ups and per-guest limits
DoneAnonymous return state
DoneExpiry, revocation and restricted-storage browsers
Revocation is already well-built: sharing off rotates the share token immediately (not just a status flag), so an old link is a dead credential at once, and both the public read and guest-follow-up routes fail closed with a friendly message rather than serving stale cached data. Fixed: a guest browser that denies localStorage (privacy mode, locked-down policy) now falls back to sessionStorage before giving up to a pure in-memory key - previously a reload after submitting a follow-up looked like it never happened and invited a duplicate resubmit. Fixed: shared-meeting links now expire on a rolling 30 days from the meeting's own last update, computed server-side and enforced everywhere a share_token is looked up (the read RPC, the guest follow-up RPC, and the recording-download route, which queried the table directly). Anchored to last-update rather than original share date, and defaults to no expiry for every already-shared meeting, so nothing that's currently shared breaks the moment this ships - it only starts counting down the next time that meeting is saved. Physical two-device evidence remains.
In progressDeployed host-and-guest two-device test
Not started
Offline and cross-device recovery
3/5Make every supported mutation converge without duplicates, loss or false success.
Connection cache and partial-source recovery
DoneAssistant deletion tombstones
DoneFeature-wide mutation and retry inventory
Inventoried every mobile mutation path. Follow-ups, quick follow-ups, offline scans, event actions and card save/delete all already queue and self-heal on reconnect. Fixed: completing or deleting a follow-up from the assistant's proposed-action card called the API directly with no offline fallback - the identical action from the ordinary Follow-ups screen already queued. Now routed through the same enqueue/dequeue helpers, so it queues and self-heals identically regardless of entry point. Remaining lower-severity paths (delete connection, delete encounter, addressed-to-me complete/reopen, contact-field requests) fail loud with input preserved and no silent loss - acceptable as-is, just inconsistent UX versus the queued paths, not a correctness gap.
In progressTermination, lost-response and conflict tests
Checked the three highest-risk mutation paths for a lost-response-then-retry scenario. Follow-up/encounter creation upserts by a client-generated id (the assistant explicitly reuses proposalId as the encounter id so a retry can't duplicate). Card save upserts by the card's own id and explicitly rejects a stale retry resurrecting an already-archived card. Event RSVP sets a fixed status rather than inserting a row, so it's naturally idempotent. Also verified: contact-field requests use an advisory-locked dedup-by-match RPC that returns the existing pending request rather than creating a duplicate. No duplicate-creation risk found in any of these.
DoneCross-device deletion and cache invalidation
Connections and Follow-ups lists already self-correct via a real 30s background refetch. Fixed: the Events list's 30s interval only ticked a "Synced Xm ago" caption, never refetched - a cancelled/updated event on another device stayed visible indefinitely as long as the screen stayed open. Fixed: Connection detail's 30s interval only refreshed that person's meetings/follow-ups, never re-checked whether the connection itself still existed - a deletion on another device left an already-open detail screen showing a person who no longer existed, fully interactive. Both now poll silently (no loading flash, no error banner on a transient failure) and only surface a change when the record is confirmed gone or updated.
In progress
Wallet, NFC and widgets
1/5Verify release-signed device surfaces that static code and simulators cannot certify.
Widget data, signed-out and refresh hardening
Widget sync fails closed, preserves the last complete iOS QR snapshot, and retains one previous asset generation while WidgetKit swaps timelines so refresh, backgrounding and app termination cannot expose a white image race. Fixed in passing: the Android widget's message button no longer builds a mailto intent from an unvalidated email string.
DoneApple Wallet physical test
Not startedGoogle Wallet physical test
Not startedNFC physical test
Not startedRelease widget entitlements and refresh
Confirmed genuinely 100% configuration/physical-device work, not code: the App Group identifier is formulaically derived from the bundle id (group.${bundleId}) rather than hand-duplicated per variant, so there's no config mismatch to catch by inspection, and staging's on-disk entitlements files already match. What remains is confirming the actual App Group entitlement and provisioning profile exist for the production bundle id in the Apple Developer portal and behave correctly on a release-signed binary - nothing a repo file can prove ahead of that build.
Not started
Release certification
1/7Record evidence, keep both web deployments aligned, and prepare separate mobile store releases.
Automated release and migration checks
The deploy-migrations workflow ran a bare `supabase db push` with no lint, dry-run or destructive-change gate at all - only a separate check-release-readiness.mjs validated environment config, never migration content. Added scripts/check-migrations.mjs: a read-only static check (never touches a database) verifying migration-file naming/ordering, balanced begin;/commit; blocks, and flagging destructive statements (drop table/column/constraint/function/policy, truncate, column type changes) for manual review rather than blocking. Wired into `npm run release:check` and as a required step in the CI deploy workflow, before the target project is even linked. Already surfaced 2 genuine pre-existing findings on the real migrations directory (both informational, not blocking).
In progressAll-user and all-state acceptance matrix
Already exists and is mature: docs/planning/CONSUMER-E2E-CERTIFICATION.md contains an extensive state-coverage matrix (identity/account, record lifecycle, connectivity, permissions, content/scale, UI/accessibility, deployment) plus required-evidence checklists and per-feature work orders. Not a gap.
DoneSentry releases, source maps and clean monitoring window
Already wired up, not missing: web uploads debug-ID-based sourcemaps via scripts/upload-sentry-sourcemaps.mjs in the Vercel build (worked around because vinext bypasses the standard webpack plugin); mobile has the @sentry/react-native/expo config plugin, DSN, and both iOS/Android sentry.properties configured, with SENTRY_DISABLE_AUTO_UPLOAD set deliberately in eas.json; docs/release-safeguards.md documents a prior symbolication bug and its fix. The remaining piece - watching a real deploy for a clean monitoring window - requires an actual production release and is not code-workable.
In progressMatching web rollout and rollback evidence
Not startedSeparate App Store and Play Store production evidence
Not startedDocument backend portability and future migration boundaries
The portability policy already existed in CONSUMER-E2E-CERTIFICATION.md but stated only intent, with no concrete inventory to check it against. The current audited snapshot covers 60 functions defined in public.* (the large majority security definer), 31 RLS-enabled tables, 2 Storage buckets (card-assets public, encounter-recordings private), 12 migration files using pg_advisory_xact_lock, confirmed zero pg_cron and zero Realtime usage (cron already runs as portable application code via Vercel Cron hitting plain API routes), and Supabase Auth identified as probably the single largest piece of future portability work. Backed by a regression test that fails if the doc's counts drift from the real migrations directory.
In progressPost-production smoke test
Not started
Business and organisation tooling is intentionally deferred while the consumer experience is certified.