Identity and subscription
GreyScreen has one user and three ways of proving who that user is. That is the single most important thing to understand about the platform's security posture.
The three models
| Surface | How identity arrives | Is it verified? |
|---|---|---|
| Website | Firebase Auth (Google) → ID token in Authorization: Bearer | Yes on /me*. Legacy /users/* and Stripe management check existence only. |
| WebView in the phone app | User object and token injected over the native bridge | No. The SPA trusts localStorage.user. |
| Phone app and TV | Google (and Apple on iOS) sign-in → POST /signup → user document | No token on most calls. Subscription is enforced client-side. |
| Admin back office | Admin JWT in an HttpOnly cookie, or Bearer | Yes, unless ADMIN_AUTH_DISABLED=true — which production does not reject. |
The website's token verification is the only strong path. The others were built for speed and are now the platform's main risk; see Security notes.
Users, emails, and the lookup guard
users/{id}uses a 32-hex document ID. That ID is an API contract: responses synthesise_idfrom it.emails/{lowercased email}holds{userId}. It is both the login lookup and the unique-email guard. Creating a user is two writes that must agree.firebaseUidlinks the website's Firebase identity to the document.MyListis keyed byfirebaseUid, not by the user document ID.
Changing any of those keys is a migration, not a refactor.
Plans
Plans live in plans/{legacy _id} with {title, priority, defaultPrice, price[], features[], duration, isEnable}. They are managed only in the admin back office and read everywhere else
through GET /subscriptions/getEnableSubscription.
priority is the only ordering mechanism:
| Convention | Where it appears |
|---|---|
| 1 = Premium, 2 = Standard, 3 = Basic | Website Constants/Constance.js, webview config/stripe.config.ts, customer API price mapping |
| 1 = Basic, 3 = Premium | Some actions/users.actions comments |
| Descending sort | PricingScreen |
The first row is what the payment code uses. The conflicting comments are a documentation defect that has not been cleaned up. When adding a plan, read the payment mapping and the plan document, never the marketing copy.
Payments
Stripe
- Hosted or embedded checkout, billing portal, cancel (immediate with proration, or at period
end), change plan (upgrade
always_invoice, downgradenone). - Webhook: raw body, signature verification, create-only dedupe in
webhookEvents, ACK200before dispatch. Events:checkout.session.completed,customer.subscription.created,updated,deleted,invoice.paid,invoice.payment_failed. - Stripe state is stored directly on the user document and reflected by
/me/subscription. - The key mode is enforced at boot:
sk_test_outside production,sk_live_in production. Price IDs carry no mode, so a misconfigured price looks valid until checkout fails.
Flutterwave
- Mobile money.
POST /pay/payByMobileMoneycreatespaymentRefs/{txRef}and calls the provider. - The webhook checks
verif-hashagainstFLW_SECRET_HASH. - The customer API dedupes through
webhookEvents. The admin API does not, and does not await several writes. Duplicate subscription records are possible. - There is no sandbox merchant: every environment charges real money. Test with the smallest possible amount, or not at all.
The customer API's payment routes are part of the legacy identity model: most of them accept a
userId and verify only that the user exists.
Subscription state
Two sources, one answer:
- Flutterwave appends
users/{uid}/subscriptionswithdateEndSubscription = now + 1 month. - Stripe writes fields on the user document;
/me/subscriptionnormalises them.
Clients ask "is this user entitled?" by calling /users/{id} (legacy) or /me (website) and
comparing the plan's priority and the end date locally.
/users/verifySubcription returns a 60-second signed JWT for any supplied user ID. Consumers
validate only the signature. It is a transport convenience, not proof of ownership, and it must
not be treated as an entitlement decision by any new service.
WebView and the bridge
Inside the phone app the webview receives:
SEND_USER_DATA_TO_WEBVIEWwith the user object;- an optional token stored in
localStorage; - navigation and cast actions in the other direction.
After a successful payment the webview polls verifySubcription and tells the shell to patch
local state. The shell, not the webview, owns sign-in and the refresh token.
Where to look
| Question | File |
|---|---|
| Website token handling | gstv-website/src/lib/authToken.ts, src/lib/apiClient.ts |
| Server-side verification | gstv-customer-api/src/middleware/firebaseAuth.middleware.ts |
| Plan → price mapping | gstv-customer-api/src/controllers/pay.controller.ts |
| Stripe webhook | gstv-customer-api/src/controllers/webhook.controller.ts |
| Flutterwave webhook | gstv-customer-api/src/controllers/pay.controller.ts, admin API's payment route |
| Admin sessions | gstv-admin-api/src/middleware/auth.middleware.ts |
Next: Data stores — what all of this is written into.