Skip to main content

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​

SurfaceHow identity arrivesIs it verified?
WebsiteFirebase Auth (Google) → ID token in Authorization: BearerYes on /me*. Legacy /users/* and Stripe management check existence only.
WebView in the phone appUser object and token injected over the native bridgeNo. The SPA trusts localStorage.user.
Phone app and TVGoogle (and Apple on iOS) sign-in → POST /signup → user documentNo token on most calls. Subscription is enforced client-side.
Admin back officeAdmin JWT in an HttpOnly cookie, or BearerYes, 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 _id from 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.
  • firebaseUid links the website's Firebase identity to the document. MyList is keyed by firebaseUid, 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:

ConventionWhere it appears
1 = Premium, 2 = Standard, 3 = BasicWebsite Constants/Constance.js, webview config/stripe.config.ts, customer API price mapping
1 = Basic, 3 = PremiumSome actions/users.actions comments
Descending sortPricingScreen

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, downgrade none).
  • Webhook: raw body, signature verification, create-only dedupe in webhookEvents, ACK 200 before 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/payByMobileMoney creates paymentRefs/{txRef} and calls the provider.
  • The webhook checks verif-hash against FLW_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:

  1. Flutterwave appends users/{uid}/subscriptions with dateEndSubscription = now + 1 month.
  2. Stripe writes fields on the user document; /me/subscription normalises 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_WEBVIEW with 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​

QuestionFile
Website token handlinggstv-website/src/lib/authToken.ts, src/lib/apiClient.ts
Server-side verificationgstv-customer-api/src/middleware/firebaseAuth.middleware.ts
Plan → price mappinggstv-customer-api/src/controllers/pay.controller.ts
Stripe webhookgstv-customer-api/src/controllers/webhook.controller.ts
Flutterwave webhookgstv-customer-api/src/controllers/pay.controller.ts, admin API's payment route
Admin sessionsgstv-admin-api/src/middleware/auth.middleware.ts

Next: Data stores — what all of this is written into.