> ## Documentation Index
> Fetch the complete documentation index at: https://docs.highailabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Users, Account & Subscription — Behavior Contract

> Every behavior the account features must have, and the test that proves each one — sign-up, onboarding, preferences, the Pro paywall, Professor High, data export, and account deletion through identity death.

## What this feature is

This is everything that makes the app *someone's*. A person signs up or signs in
with an email code, Apple, or Google; a Convex user record is created for them
behind the scenes; and the app decides on launch whether to show them onboarding or
drop them straight home. From there they edit their profile, set consumption
preferences and a theme, and see whether they are on the free tier or **Pro** —
which is what opens Professor High, notebook generation, receipt parsing and the
Shopping Agent. They can chat with **Professor High**, which reads their own stash,
orders and collection to answer. And they own the exit: **Export All Data** writes
every row the app holds about them to a file, and **Delete Account** removes all of
it — Convex rows, uploaded files, connected Gmail access, and the Clerk identity
itself.

## How to read the contract

Every case ID below is a live test. Cases built during the feature-test programme
carry their ID in the test title, so `grep 'SUB-029'` finds the assertion. Rows
whose coverage predates the programme are cited by ID and file; the test is found by
its plain-English name in that file. A few behaviors landed in wave 3 under a defect
id — those cite `BUG-nnn`, which is likewise the literal prefix of the test title.

## The behavior contract

### Signing up and signing in

| Behavior                                                                                                                                                                                                                                  | Verified by                                      | Tier(s) | Where (file basename)                                                                                         |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------- |
| The email-code sign-up flow handles a fresh address, an address already taken, and a stale attempt as three distinct outcomes.                                                                                                            | AUTH-005                                         | B       | `auth-flows.test.ts`                                                                                          |
| Verifying a code binds the new account's ownership **before** the session is activated, so nothing can attach to the wrong subject.                                                                                                       | AUTH-006                                         | B       | `auth-flows.test.ts`                                                                                          |
| Apple and Google sign-in mark a fresh sign-up only when an account was actually created, not on every sign-in.                                                                                                                            | AUTH-007                                         | B       | `oauth-sign-in.test.ts`                                                                                       |
| Double-tapping submit, or an OAuth tap landing in the same tick, still reaches exactly one terminal state.                                                                                                                                | AUTH-008                                         | B       | `auth-attempt-coordination.test.ts`                                                                           |
| The auth screens are reachable only while signed out, a loading state is shown until Clerk has loaded, a production build refuses a Clerk **test** key, and the official token cache is the one actually wired in.                        | AUTH-001, AUTH-002, AUTH-003, AUTH-004           | B       | `RootLayoutNav.helpers.test.ts`, `clerk-key-environment-guard.test.ts`, `AppProviderTree.token-cache.test.ts` |
| Backend subscriptions are skipped while auth is unresolved and for guests, so a guest never issues an authenticated query.                                                                                                                | AUTH-011                                         | B       | `useConvexAuthState.test.ts`                                                                                  |
| Signing out clears the API token, Clerk's storage, the image cache and any order drafts in that order — and if Clerk fails mid-sign-out, credentials are preserved rather than half-cleared.                                              | AUTH-009, AUTH-010                               | B       | `sign-out-service.test.ts`                                                                                    |
| The end-to-end test bypass fails closed on a production build and with a missing token, succeeds on staging only for a `+clerk_test` identity whose email matches exactly, and every test-only fixture refuses to run against production. | AUTH-015, AUTH-017, AUTH-018, AUTH-019, AUTH-020 | A, B, C | `test-session.test.ts`, `users.featuretests.test.ts`, `users.integration.test.ts`                             |

### Getting a user record

| Behavior                                                                                                                                                                                                                                               | Verified by               | Tier(s) | Where (file basename)                         |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | ------- | --------------------------------------------- |
| The Clerk webhook accepts a validly signed payload and rejects a replayed, mismatched or missing signature — and an oversized or chunked body is rejected before verification is even attempted.                                                       | USR-001, USR-002          | A       | `clerkWebhook.test.ts`                        |
| A new Clerk user becomes a user record on the free tier; a record created lazily by a subscription write is enriched rather than duplicated; and a profile update for a user we have never seen creates the record defensively instead of dropping it. | USR-003, USR-004, USR-005 | A       | `users.featuretests.test.ts`                  |
| A profile update clears a name or avatar the user really cleared, but **an absent email is no news, not news of absence** — every writer of the mirrored address replaces it with another address and never erases it.                                 | USR-006, BUG-157          | A       | `users.featuretests.test.ts`                  |
| Profile fields in Convex are a read-only mirror: the Clerk webhook writes them and nothing else does.                                                                                                                                                  | PRF-008                   | A       | `users.featuretests.test.ts`                  |
| While an account deletion is in flight, create and update webhooks are ignored, so a late event cannot resurrect the account mid-purge.                                                                                                                | USR-007                   | A       | `users.featuretests.test.ts`                  |
| Once a deletion has completed, signing up again with the same identity really does create a new user.                                                                                                                                                  | USR-008                   | A       | `users.featuretests.test.ts`                  |
| The webhook route dispatches created, updated and deleted to the right handler, and deletion goes to the coordinator — which restarts a completed retention tombstone rather than skipping it and cleaning up nothing.                                 | USR-009, USR-010, BUG-114 | A       | `users.featuretests.test.ts`, `users.test.ts` |

### First run and onboarding

| Behavior                                                                                                                                                                                                                                                        | Verified by               | Tier(s) | Where (file basename)                                               |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------- | ------------------------------------------------------------------- |
| Guests, users who have finished onboarding, and users who already have data all launch straight to Home; onboarding is shown only to a resolved, signed-in, genuinely empty account; and a decision that cannot be made within four seconds fails safe to Home. | ONB-001, ONB-002, ONB-003 | B       | `resolveLaunchRoute.test.ts`                                        |
| When one signed-in user replaces another, the first user's completion state is never rendered for the second.                                                                                                                                                   | ONB-004                   | B       | `useLaunchRoute.test.tsx`                                           |
| "Onboarding complete" is stored per Clerk subject, never once per device.                                                                                                                                                                                       | ONB-005                   | B       | `onboarding-completion-storage.test.ts`                             |
| The onboarding preview flows are unavailable outside preview environments, and exiting a preview writes no completion marker while a real finish does.                                                                                                          | ONB-006, ONB-007          | B       | `onboardingOwnership.test.ts`                                       |
| The onboarding screen's calls-to-action route to their four documented destinations, and the Historical Import card is hidden when the backend says it is not available.                                                                                        | ONB-008, ONB-009          | B       | `UsersOnboarding.test.tsx`                                          |
| Tapping "notify me" as a guest short-circuits without a mutation, registering interest rejects a key that is not in the catalog, and removal still accepts a legacy key.                                                                                        | ONB-011, ONB-013, ONB-014 | A, B    | `featureInterests.featuretests.test.ts`, `featureInterests.test.ts` |

### Profile

| Behavior                                                                                                                                                                                                                      | Verified by               | Tier(s) | Where (file basename)              |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------- | ---------------------------------- |
| Editing a name or username trims it and clearing the username clears it, a new photo is uploaded and then the record reloaded so the new image is what the app shows, and profile completion counts every field it claims to. | PRF-001, PRF-002, PRF-003 | B       | `profile.helpers.test.ts`          |
| The primary email can never be removed, and removing the last verified factor is blocked.                                                                                                                                     | PRF-004                   | B       | `profile-identity.helpers.test.ts` |
| A failed profile save can be retried and then succeeds, without stranding the screen.                                                                                                                                         | PRF-005                   | B       | `profile-screen-lifecycle.test.ts` |
| The profile tab keeps its row order and destinations at the largest Dynamic Type sizes.                                                                                                                                       | PRF-006                   | B       | `ProfileTabDynamicType.test.tsx`   |

### Preferences and theme

| Behavior                                                                                                                                                                                                                                            | Verified by                  | Tier(s) | Where (file basename)                                                                                                  |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| All seven preference functions reject an unauthenticated caller, and preferences are scoped strictly per user.                                                                                                                                      | PREF-001, PREF-002           | A       | `users.featuretests.test.ts`, `userPreferences.test.ts`                                                                |
| Saving preferences creates the record the first time and updates it in place afterwards, and each individual field updater also creates the record when none exists.                                                                                | PREF-003, PREF-004           | A       | `userPreferences.test.ts`                                                                                              |
| Updating a consumption method replaces the matching entry and appends a genuinely new one; an empty save leaves every existing value untouched; and deleting preferences a user never had is a no-op.                                               | PREF-005, PREF-006, PREF-007 | A       | `users.featuretests.test.ts`                                                                                           |
| A value outside the allowed set is rejected by the validator rather than stored.                                                                                                                                                                    | PREF-008                     | A       | `users.featuretests.test.ts`                                                                                           |
| The preferences hook sends exactly the arguments each validator accepts, the screen renders loading, empty and populated as three distinct states, and the frequency control maps the stored value to the on-screen segment and back without drift. | PREF-009, PREF-010, PREF-011 | B       | `useConsumptionPreferences.featuretests.test.tsx`, `UsersSettings.test.tsx`, `consumption-preferences.helpers.test.ts` |
| Choosing a theme persists it, and an unrecognized stored value normalizes to "system" rather than breaking the app.                                                                                                                                 | PREF-013                     | B       | `theme-selection.helpers.test.ts`, `ThemeSelectionSheet.test.tsx`                                                      |

### Knowing whether you are Pro

| Behavior                                                                                                                                                                                                            | Verified by               | Tier(s) | Where (file basename)                                             |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------- | ----------------------------------------------------------------- |
| A signed-out reader gets no subscription, a signed-in user with no record reads as free with no expiry and no historical-import access, and an active Order Import Lab grant reports Pro with the lab's own expiry. | SUB-001, SUB-002, SUB-004 | A       | `users.featuretests.test.ts`                                      |
| A stored Pro tier whose expiry has passed reads as free — the stored tier is remembered but not honoured.                                                                                                           | SUB-003, SUB-111          | A, C    | `users.featuretests.test.ts`, `subscriptions.integration.test.ts` |
| The subscription query is skipped entirely for guests, the development bypass short-circuits before any query, and "expiring soon" is recomputed when the day rolls over rather than only on mount.                 | SUB-031, SUB-032          | B       | `useSubscription.featuretests.test.tsx`                           |
| The billing screen renders active, expiring, free and loading distinctly, and the manage-billing action opens the customer centre and surfaces its failure.                                                         | SUB-035, SUB-036          | B       | `UsersSubscription.test.tsx`                                      |
| A gated screen shows the Pro upgrade instead of mounting protected content, and renders non-committally — neither content nor paywall — while entitlement is still loading.                                         | SUB-037, SUB-038          | B       | `AccessGate.test.tsx`, `UsersSubscription.test.tsx`               |

### Syncing entitlement from the store

| Behavior                                                                                                                                                                                                                                              | Verified by                        | Tier(s) | Where (file basename)                   |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- | --------------------------------------- |
| A sync with no identity is rejected, and **the sync cannot target another account — no argument naming one exists**.                                                                                                                                  | SUB-005, SUB-006                   | A       | `users.featuretests.test.ts`            |
| When the store's key is configured the store's own verdict beats whatever tier the client claimed, a lifetime entitlement is active Pro with no expiry, and a grace period running past the stated expiry wins.                                       | SUB-007, SUB-012, SUB-013          | A       | `users.featuretests.test.ts`            |
| An unknown subscriber resolves to free without throwing, a store outage is refused and writes nothing, and on a real deployment with no store secret the sync fails closed — only a local development runtime trusts the client's claim, and says so. | SUB-008, SUB-009, SUB-010, SUB-011 | A       | `users.featuretests.test.ts`            |
| The client retries a failed sync on a fixed schedule and then stops without crashing, and a refused sync never clears entitlement state the app already holds.                                                                                        | SUB-033, SUB-034                   | B       | `useSubscription.featuretests.test.tsx` |
| No subscription state is written at all while an account deletion is in flight.                                                                                                                                                                       | SUB-014                            | A       | `subscriptions.test.ts`                 |

### The store webhook

| Behavior                                                                                                                                                                                                                                                | Verified by                        | Tier(s) | Where (file basename)                                      |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- | ---------------------------------------------------------- |
| Purchase, renewal, cancellation and expiration each move the account to the right tier, and an event arriving out of order is ignored while a newer one is applied — a late cancellation cannot undo a fresh renewal.                                   | SUB-015, SUB-016                   | A       | `revenuecat-webhook.test.ts`                               |
| An invalid signing secret is refused, a payload with no subscriber is rejected, and a body over the limit is refused outright.                                                                                                                          | SUB-017, SUB-024                   | A       | `revenuecat-webhook.test.ts`, `users.featuretests.test.ts` |
| A transfer grants Pro to the account receiving it and **removes it from the account it came from** — including in the real payload shape that names neither account in the usual field — and never fabricates an entitlement for an anonymous store id. | SUB-018, SUB-019, SUB-046, SUB-047 | A       | `users.featuretests.test.ts`                               |
| Product change, un-cancellation and billing issue are each handled rather than ignored.                                                                                                                                                                 | SUB-020                            | A       | `users.featuretests.test.ts`                               |
| The one-time historical-import purchase grants that entitlement without touching the subscription tier and is revoked on expiration, refund or transfer; and a product whose id merely contains "pro" is not treated as Pro.                            | SUB-021, SUB-022, SUB-023          | A       | `users.featuretests.test.ts`                               |

### The server-side Pro gate

| Behavior                                                                                                                                                                                                                                                             | Verified by                | Tier(s) | Where (file basename)                                                                                    |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| The entitlement endpoint returns the correct verdict for a Pro and a free account, and rejects a wrong or absent bearer, a too-short secret, extra keys, or a padded user id.                                                                                        | SUB-025, SUB-027, SUB-028  | A       | `users.featuretests.test.ts`, `entitlementHttp.test.ts`                                                  |
| The gate is enforced unless **both** the bypass variable is set **and** the runtime is non-production, and the Order Import Lab self-grant refuses to run on production or on any unidentified remote deployment.                                                    | SUB-029, SUB-030           | A       | `users.featuretests.test.ts`                                                                             |
| A bypassed deployment is detectable rather than silently permissive: the entitlement endpoint short-circuits visibly, and the live suites record the deployment's enforcement state before asserting anything against it.                                            | SUB-026, SUB-044, SUB-100  | A, C    | `users.featuretests.test.ts`, `users.integration.test.ts`, `subscriptions.integration.test.ts`           |
| A free account calling a Pro API route gets **402 — not 401 and not 400** — so the app can tell "you need Pro" apart from "you are signed out" and from "your request was malformed", and the client classifies that 402 as a paywall rather than a generic failure. | SUB-042, SUB-104, CHAT-024 | B, C    | `users.integration.test.ts`, `subscriptions.integration.test.ts`, `use-chat-engine.featuretests.test.ts` |
| A Pro account reaches the validator instead of the paywall — the same route answers 400 on a bad body, proving the gate opened.                                                                                                                                      | SUB-107, SUB-108           | C       | `subscriptions.integration.test.ts`                                                                      |
| A Pro-gated Convex surface refuses a free caller with the paywall error, and receipt parsing refuses **before** the paid call is made.                                                                                                                               | SUB-102, SUB-103           | C       | `subscriptions.integration.test.ts`                                                                      |

### Feature flags

| Behavior                                                                                                                             | Verified by        | Tier(s) | Where (file basename)               |
| ------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | ------- | ----------------------------------- |
| The public flag list exposes only a key and whether it is on, and is readable while signed out so a guest sees the same feature set. | FLAG-001, FLAG-002 | A       | `users.featuretests.test.ts`        |
| Setting an override replaces the existing one instead of accumulating duplicates, and removing an unknown key is a no-op.            | FLAG-003, FLAG-004 | A       | `users.featuretests.test.ts`        |
| A backend-read flag defaults to on and is off only on an explicit `false`.                                                           | FLAG-005           | A       | `users.featuretests.test.ts`        |
| Precedence is fixed: a build-time environment flag beats a remote override, which beats the registry default.                        | FLAG-006           | B       | `featureFlags.featuretests.test.ts` |

### Professor High

| Behavior                                                                                                                                                                                                                                                  | Verified by                            | Tier(s) | Where (file basename)                                        |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------- | ------------------------------------------------------------ |
| Every chat-session function rejects an unauthenticated caller, one person can never read, update, activate or delete another's conversation, and malformed, oversized or over-count transcripts are rejected rather than silently truncated.              | CHAT-001, CHAT-002, CHAT-003           | A       | `chatSessions.test.ts`                                       |
| Saving a conversation does not make it the active one, and a late save cannot steal the active slot from the conversation the user is actually in.                                                                                                        | CHAT-004, CHAT-005                     | A       | `chatSessions.test.ts`                                       |
| The fifty-conversation cap prunes oldest-first, an account already over the cap shrinks monotonically, two concurrent saves converge without exceeding it, and hitting the daily write quota leaves the stored conversation unchanged.                    | CHAT-006, CHAT-007, CHAT-011           | A       | `chatSessions.test.ts`, `users.featuretests.part2.test.ts`   |
| The 180-day retention sweep never deletes the conversation the user is currently in, the history sheet can reach every stored conversation up to the cap, and deleting the active conversation clears the active pointer rather than leaving it dangling. | CHAT-008, CHAT-009, CHAT-010           | A       | `users.featuretests.part2.test.ts`                           |
| A completed turn is saved and claims the active slot, a conversation the user deleted is never written even on unmount, replacing one signed-in user with another clears the input, transport and pending save, and sending is blocked offline.           | CHAT-019, CHAT-020, CHAT-021, CHAT-022 | B       | `use-chat-engine.test.tsx`                                   |
| The client trims the outgoing transcript to stay inside the backend's size and message ceilings.                                                                                                                                                          | CHAT-023                               | B       | `use-chat-engine.featuretests.test.ts`                       |
| The chat screen renders disabled, upgrade-required, restoring and empty as four distinct states, every rendered element carries a known test id, and opening a conversation from history passes an opaque id with no route push.                          | CHAT-025, CHAT-026, CHAT-027           | B       | `UsersChat.test.tsx`, `ProfessorHighLayerOwnership.test.tsx` |
| The assistant's data reads return exactly the documented projection for each of the seven sources, and its stats exclude drafts.                                                                                                                          | CHAT-012, CHAT-013                     | A       | `users.featuretests.part2.test.ts`                           |
| The assistant favoriting a strain on the user's behalf toggles it, records the interaction, and creates the collection row when none exists.                                                                                                              | CHAT-014, CHAT-015                     | A       | `users.featuretests.part2.test.ts`                           |
| The assistant's context endpoint rejects a wrong bearer and an invalid data-type request and returns only the keys asked for, and its action endpoint rejects unknown actions and incomplete payloads.                                                    | CHAT-016, CHAT-017, CHAT-018           | A       | `users.featuretests.part2.test.ts`                           |

### Exporting your data

| Behavior                                                                                                                                                                          | Verified by               | Tier(s) | Where (file basename)                                           |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------- | --------------------------------------------------------------- |
| Every export section has an executable policy and every table an explicit decision — a new table cannot be silently omitted from the export.                                      | EXP-001                   | A       | `dataExport.test.ts`                                            |
| An unauthenticated export and an invalid page size are both rejected, one user's export contains only their rows, and server and provider identifiers are redacted out of it.     | EXP-002, EXP-003          | A       | `dataExport.test.ts`                                            |
| The export is a fixed point in time — rows created or changed after it starts are not in it.                                                                                      | EXP-004                   | A       | `dataExport.test.ts`                                            |
| Notebook children are reachable only through an owner-verified parent, and journal photos are exported as retrievable URLs, never as internal storage ids.                        | EXP-005, EXP-006          | A       | `dataExport.test.ts`                                            |
| A page request against a foreign or expired export is refused, finishing an export deletes only the caller's own snapshot, and an abandoned one is deleted only after it expires. | EXP-007, EXP-009, EXP-010 | A       | `users.featuretests.part2.test.ts`                              |
| Starting an export is refused while an account deletion exists.                                                                                                                   | EXP-008, DEL-026          | A, C    | `users.featuretests.part2.test.ts`, `users.integration.test.ts` |
| The app streams all twenty-seven sections into valid JSON and removes its temporary file, and cancelling mid-export cleans up without opening the share sheet.                    | EXP-011, EXP-012          | B       | `data-export.test.ts`                                           |
| The export hook sends exactly the argument shapes the three export functions validate.                                                                                            | EXP-013                   | B       | `useDataExport.featuretests.test.tsx`                           |

### Deleting your account

| Behavior                                                                                                                                                                                                                                                                                                                                                 | Verified by                        | Tier(s) | Where (file basename)                                         |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- | ------------------------------------------------------------- |
| Deletion cannot be initiated unauthenticated, and no cross-user identifier is ever accepted.                                                                                                                                                                                                                                                             | DEL-001                            | A       | `accountDeletion.test.ts`                                     |
| The snapshot walks every notebook the user owns exactly once, the purge runs in bounded resumable batches that never touch another owner's rows, and a write landing after its route was purged restarts a full sweep rather than leaving a stray row.                                                                                                   | DEL-002, DEL-003, DEL-004          | A       | `accountDeletion.test.ts`                                     |
| A stored file whose key cannot be resolved fails the run closed, before the purge — deletion never reports success over a file it could not find.                                                                                                                                                                                                        | DEL-005                            | A       | `accountDeletion.test.ts`                                     |
| A second worker cannot claim a deletion while the first still holds it, and a completed deletion leaves a tombstone retained until its expiry, re-arming itself while retention remains and removed once it passes.                                                                                                                                      | DEL-006, DEL-007, DEL-008, DEL-009 | A       | `accountDeletion.test.ts`, `users.featuretests.part2.test.ts` |
| A full run empties every purge route, and a retryable failure resumes at the phase it failed in and clears the previous error.                                                                                                                                                                                                                           | DEL-014, DEL-015                   | A       | `users.featuretests.part2.test.ts`                            |
| Retry backoff grows monotonically to a five-minute ceiling, and an exhausted deletion stops rescheduling and reports itself as not retryable rather than looping forever.                                                                                                                                                                                | DEL-016, DEL-027                   | A       | `users.featuretests.part2.test.ts`                            |
| A completed tombstone is not reported to the app as a live deletion.                                                                                                                                                                                                                                                                                     | DEL-017                            | A       | `users.featuretests.part2.test.ts`                            |
| External cleanup is exact: the background research run is cancelled idempotently and keeps its locator when ambiguous, file deletion touches only allow-listed paths the user owns, Clerk deletion targets only the job's own subject and tolerates an already-deleted identity, and Gmail access is revoked without the token ever reaching the caller. | DEL-010, DEL-011, DEL-012, DEL-013 | A       | `accountDeletionExternal.test.ts`                             |
| **The app never leaves a signed-in session over a deleted account**: sign-out is retried with exponential backoff up to a ceiling and never given up on, and returning to the foreground restarts the ladder immediately.                                                                                                                                | DEL-018, BUG-084                   | B       | `AccountDeletionSessionGuard.test.tsx`                        |
| The guard never adopts one user's deletion after a different user has signed in.                                                                                                                                                                                                                                                                         | DEL-019                            | B       | `AccountDeletionSessionGuard.test.tsx`                        |
| Local state is cleared only after the backend has accepted the deletion, and the confirmation copy stays explicitly irreversible.                                                                                                                                                                                                                        | DEL-020, DEL-021                   | B       | `profile.helpers.test.ts`                                     |

### Daily Stories

Daily Stories is quarantined by design; its full contract lives on the
[Notebooks & Daily Stories](/planning/testing/specs/notebooks) page. These rows are
the account-side half.

| Behavior                                                                                                                                                                                                                                                                                                                                                                                          | Verified by                                           | Tier(s) | Where (file basename)                                                                                                                          |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Every daily-story query rejects an unauthenticated caller, and one person can neither see nor mark viewed another person's story.                                                                                                                                                                                                                                                                 | STORY-001, STORY-005                                  | A       | `dailyStories.test.ts`                                                                                                                         |
| A ready story is withheld from its own owner while the feature is quarantined, marking it viewed records nothing, and the nightly job creates no work even for an eligible Pro account.                                                                                                                                                                                                           | STORY-002, STORY-003, STORY-004                       | A       | `dailyStories.test.ts`                                                                                                                         |
| Month and month-range reads hide quarantined rows across a year boundary and return nothing for a non-positive month count, and the week read rejects more than seven keys and non-calendar keys, dedupes keys, and preserves the caller's order.                                                                                                                                                 | STORY-006, STORY-007, STORY-008, STORY-009            | A       | `dailyStories.test.ts`, `users.featuretests.part2.test.ts`                                                                                     |
| Both daily-story webhooks are terminally retired — 410 with a bearer, 401 without.                                                                                                                                                                                                                                                                                                                | STORY-010                                             | A       | `users.featuretests.part2.test.ts`                                                                                                             |
| The week hook derives its date keys in the app's own timezone rather than UTC and skips both subscriptions for guests and when the flag is off; a permanently empty backend hides the header strip instead of showing a skeleton forever; the history screen renders a stable empty calendar with no load-more loop; and the viewer records exactly two views, one on open and one on completion. | STORY-012, STORY-013, STORY-014, STORY-015, STORY-016 | B       | `useDailyStories.test.ts`, `useDailyStories.featuretests.test.tsx`, `UsersDailyStories.test.tsx`, `StoryViewerContainer.featuretests.test.tsx` |

### Everything an account owns

| Behavior                                                                                                                                                                                   | Verified by | Tier(s) | Where (file basename)              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ------- | ---------------------------------- |
| Every table this area writes is on the account-purge list **and** either exported or explicitly excluded — a new table cannot be added without a deletion decision and an export decision. | X-002       | A       | `users.featuretests.part2.test.ts` |
| The chat-session ownership index is purged on account deletion.                                                                                                                            | X-003       | A       | `users.featuretests.part2.test.ts` |

## What the live tier proves end-to-end

Tier C signs in as a real, disposable `+clerk_test` identity against the deployed
staging Convex deployment and the production Hono API, then destroys the identity.

* **Enforcement is measured before anything is asserted against it** — the suite records whether staging is enforcing the subscription gate, so no paywall result can pass for the wrong reason (SUB-044, SUB-100).
* **A brand-new identity really is new**: it receives a Convex JWT, calls an authenticated query, has no data anywhere, and its lifetime stats are fully zeroed (AUTH-012, ONB-010, PRF-007).
* **Authentication holds on the deployed backend** — every hard-auth function rejects an unauthenticated client, and the soft-auth queries return their guest shape rather than erroring (AUTH-013, AUTH-014).
* **All three subscription states behave on the real deployment, and so do both transitions**: free closes every gate, active Pro opens every gate, a lapsed account closes them again exactly as a free one does, and a past expiry demotes a stored Pro tier (SUB-039–SUB-041, SUB-101, SUB-106, SUB-112, SUB-113, SUB-114).
* **The deployed API answers 402 for a free identity and streams for a Pro one**, the entitlement endpoint's projection agrees with both, and one real paid receipt parse completes end to end (SUB-042, SUB-043, SUB-105, SUB-109, SUB-110).
* **Preferences, feature interests and chat sessions round-trip against the deployed validators**, an oversized transcript is refused by the real backend, and a Professor High turn really streams and really persists (ONB-012, CHAT-028, CHAT-029, CHAT-031).
* **A full export of a seeded identity works**: one snapshot, all its pages, a clean finish, the seeded rows present, no foreign rows, and the app's section list matching what the deployed backend actually pages (EXP-014, EXP-015, EXP-016).
* **Account deletion completes for real, through identity death** — the job reaches `completed`, every user-owned query afterwards returns empty, the Clerk identity is gone, and an export cannot be started while the deletion is live (DEL-022, DEL-023, DEL-024, DEL-026).
* **One disposable identity runs the whole lifecycle in order** — create, onboard, set preferences, register interest, chat, export, delete (X-001).

## Deliberate gaps & conditions

* **Six live cases are blocked on three webhook secrets that only John can set** on
  the staging deployment. Until they are present these skip with their reason named,
  which is not a pass:
  * `CLERK_WEBHOOK_SECRET`, plus pointing the Clerk development instance at the
    staging webhook URL → USR-012, USR-013, DEL-025. Without it, **no user record is
    created on a real staging sign-up**, and the webhook deletion backstop never
    fires.
  * `REVENUECAT_WEBHOOK_SECRET` → SUB-045. The store-webhook normalizer has no live
    coverage, and its transfer handling changed most recently.
  * `PROFESSOR_HIGH_WEBHOOK_SECRET` → PREF-012, CHAT-030. The server-side assistant
    context path has never run against a real deployment.
* **Remote feature-flag delivery is not proven on staging.** FLAG-007 would require
  writing a flag on a deployment every other suite shares, so override precedence is
  proven at Tier A and B only.
* **Two chat behaviors are deliberately unbuilt at the live tier**: a tool call
  depends on the model choosing to make one (CHAT-032), and proving the per-user chat
  rate limit costs twenty-one real AI requests (CHAT-033).
* **Two internal diagnostics are not asserted as cases**: the internal data-count
  query has no user-facing surface (USR-011), and the dead `hasUsedFreeNotebook`
  field was resolved by removing it — the surviving pin asserts the field is gone
  from the schema and has no consumers, in `usersSchemaStatic.test.ts` (X-004).
* **Two daily-story behaviors are proven in the notebooks suite** rather than twice:
  the view-streak read (STORY-011 → NB-091) and the staging cron producing nothing
  (STORY-018 → NB-228).
* **Theme, onboarding completion and the mini-player preference are device-local.**
  They live in local key-value storage and have no backend, so nothing about them is
  asserted against a deployment — and onboarding completion is deliberately keyed per
  Clerk subject so sharing a device does not share it.
* **Sign-up on staging currently creates no user record.** Because the Clerk webhook
  secret is absent, USR-012 polls for the row rather than asserting it: that case is
  the detector for the configuration gap, not a product test.
* **Tier C skips cleanly without credentials**, naming exactly what is missing, and
  is never part of `pnpm test`, `pnpm check`, or a cached Turbo run.

## Where to extend

A new account, preference, subscription, flag or chat rule gets its Tier A case in
`apps/mobile/convex/tests/users.featuretests.test.ts` — or
`users.featuretests.part2.test.ts` for deletion, export, chat and daily-story rules,
which is where that half of the area lives. A new hook-argument or screen-state rule
goes in the co-located `*.featuretests.test.ts(x)` next to its hook
(`useSubscription`, `useConsumptionPreferences`, `useDataExport`, `use-chat-engine`,
`featureFlags`) or in `apps/mobile/test/ui/features/Users*.test.tsx` for rendering.
Reach for `apps/mobile/test/integration/suites/users.integration.test.ts` — or
`subscriptions.integration.test.ts` for anything about the paywall — only when the
claim is about the **deployed** backend, and remember that deletion is destructive:
it must be the last thing an identity does. Then add the row to the master matrix;
see [Mobile Feature Test Platform](/planning/testing/feature-test-platform) for
naming, tags, and the tier decision.
