Activity log

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.

0

Stabilise the current foundation

5/6

Put 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."

    Done
  • Deploy 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.

    Done
  • Verify 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.

    Done
  • Run 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 progress
  • Keep 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.

    Done
  • Create 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
1

AI assistant

4/6

Make every conversation and action dependable across supported mobile devices.

  • Conversation storage, reopening and deletion

    Done
  • Workspace-scoped context and memory boundaries

    Done
  • Capture, 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.

    Done
  • Voice, 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 progress
  • All 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.

    Done
  • Long-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
2

Notifications and Inbox

4/5

Audit every producer, preference, badge, destination and actionable notification as one system.

  • Event, nudge and Reach Out routing

    Done
  • Inventory 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.

    Done
  • Unread, 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.

    Done
  • Exact 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.

    Done
  • Offline, 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
3

Authentication and onboarding

2/5

Give 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 progress
  • Session 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 progress
  • Visitor 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.

    Done
  • First 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.

    Done
  • Cross-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
4

Scanning and connection exchange

2/5

Certify the complete two-person QR and connection loop on real devices.

  • Atomic, rate-limited public exchange

    Done
  • Connection ownership and duplicate protection

    Done
  • Camera, 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 progress
  • Offline 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 progress
  • Two-account physical-device exchange

    Not started
5

Connected accounts and contact requests

2/5

Make imported information and missing-detail recovery reliable without weakening privacy.

  • Contact-request ownership and deduplication

    Done
  • Google 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 progress
  • Calendar 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 progress
  • Fulfilled 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 progress
  • Provider 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
6

Account, profile and data controls

2/5

Verify that every user can understand, update, export and remove their data safely.

  • Profile and primary-card photo linkage

    Done
  • Notification 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 progress
  • Pending 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.

    Done
  • Accessibility 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 progress
  • Full 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
7

Shared meetings and guests

3/5

Prove that public sharing is useful, private and resilient for anonymous and returning guests.

  • RPC privacy boundary

    Done
  • Atomic guest follow-ups and per-guest limits

    Done
  • Anonymous return state

    Done
  • Expiry, 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 progress
  • Deployed host-and-guest two-device test

    Not started
8

Offline and cross-device recovery

3/5

Make every supported mutation converge without duplicates, loss or false success.

  • Connection cache and partial-source recovery

    Done
  • Assistant deletion tombstones

    Done
  • Feature-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 progress
  • Termination, 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.

    Done
  • Cross-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
9

Wallet, NFC and widgets

1/5

Verify 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.

    Done
  • Apple Wallet physical test

    Not started
  • Google Wallet physical test

    Not started
  • NFC physical test

    Not started
  • Release 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
10

Release certification

1/7

Record 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 progress
  • All-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.

    Done
  • Sentry 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 progress
  • Matching web rollout and rollback evidence

    Not started
  • Separate App Store and Play Store production evidence

    Not started
  • Document 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 progress
  • Post-production smoke test

    Not started

Business and organisation tooling is intentionally deferred while the consumer experience is certified.