You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bb36239
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/architecture.md
+11Lines changed: 11 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -113,6 +113,8 @@ Deletion runs in a fixed order, and a failure at any step leaves the account in
113
113
3. Call `prepare_account_deletion`, which deletes the workspaces the user owns and the user's own sources, threads, messages, and results in other workspaces. Those foreign keys stay `RESTRICT` so that a raw delete of an auth user cannot remove shared work by accident.
114
114
4. Delete the auth user with the Auth admin API. Every other foreign key into `auth.users` cascades or sets null, and the `on_auth_user_deleted` trigger clears the user from announcements, which have no foreign key.
115
115
116
+
The server cannot cancel an App Store subscription. Deletion removes the user's `app_store_subscriptions` rows, but Apple keeps billing until the user cancels in their Apple ID's subscription settings. The deletion response's `appStoreSubscriptionActive` says when Apple may still bill (not revoked, auto-renew not off, and ended less than 60 days ago, Apple's longest billing retry), and the app must then send the user to those settings. Notifications for the deleted subscription are acknowledged and ignored.
117
+
116
118
Admin-authored rows such as promo codes, curated sources, announcements, and grants to other users survive with the author cleared. Stripe keeps its own invoices and payments for tax and accounting. A new foreign key to `auth.users` must cascade, set null, or be cleared by `prepare_account_deletion`. `supabase/tests/account_deletion_test.sql` fails if its fixture has no row in that table or if any row still mentions the deleted user.
117
119
118
120
## Authentication and authorization
@@ -130,6 +132,15 @@ Supabase Auth owns user sessions. Protected routes provide a client navigation b
130
132
131
133
Checkout and portal functions create Stripe sessions after authenticated ownership and redirect validation. The webhook updates subscription state and records event processing for idempotency. AI handlers record the provider-reported usage and charge for each request.
132
134
135
+
A plan can come from several sources, and one database function merges them. `public.effective_entitlement(user_id)` in [`20261014123154_app_store_entitlements.sql`](../supabase/migrations/20261014123154_app_store_entitlements.sql) returns the highest active tier (pro, then legacy_free, basic, trial, expired), its daily limit (50, 50, 20, 5, 0, always replaced by `daily_limit_override`), whether it includes Pro models, the source and its expiry. The sources are `user_settings` (trial, promo and admin grants, legacy access, and the `pro` flag the Stripe webhook sets), Stripe `subscriptions` (counted only while that flag is `pro`), and `app_store_subscriptions`. A missing settings row fails closed. Every tier read goes through it: the AI and voice quota checks (`_shared/entitlements.ts`), the website's `useSubscription` and settings store, and the app, through `readEntitlement` in [`packages/core/src/supabase/entitlement.ts`](../packages/core/src/supabase/entitlement.ts). It runs with the caller's privileges, so a signed-in user reads only their own plan. The admin pages still show and edit the stored `subscription_tier` column.
136
+
137
+
The app sells Basic and Pro through StoreKit, and a subscription bought on either the website or the App Store unlocks both. Two Edge Functions write App Store subscriptions, both through `apply_app_store_transaction`, after verifying Apple's JWS signature chain against the pinned Apple Root CA - G3 ([`_shared/app-store.ts`](../supabase/functions/_shared/app-store.ts)) and the bundle id and environment:
138
+
139
+
-`app-store-notifications` receives App Store Server Notifications V2. Each notificationUUID is applied once, and Apple's retries are acknowledged.
140
+
-`app-store-purchase` takes the signed transaction the app gets from StoreKit, and records it only when its `appAccountToken` equals the caller's user id. The app sets that token to the user id at purchase.
141
+
142
+
A subscription row is linked to a user once, by that token, and later transactions update it in purchase-date order. A transaction Apple marks `isUpgraded` grants nothing, as in StoreKit's current entitlements, until the upgrade that replaces it arrives. Product ids map to tiers in `app_store_products`. Until App Store Connect has the real products, that table holds placeholders.
143
+
133
144
All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared ten-model catalog, Luna Basic default, Pro-only entries, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge.
134
145
135
146
`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts and normally resolves to GLM 5.3 Flash; manual GLM selection keeps its Pro restriction. Auto Pro remains Pro-only. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit.
0 commit comments