Architecture overview
The TwistCue Admin is one of three surfaces on top of the same NestJS API and Postgres database.
High-level
Section titled “High-level”graph TB subgraph Clients MOBILE[Mobile app<br/>React Native/Expo] ADMIN[Admin panel<br/>Next.js 16] DOCS[Docs site<br/>Astro Starlight] end
subgraph Core API[NestJS API<br/>/v1/*] PG[(Postgres 15)] REDIS[(Redis 7)] end
subgraph Providers CF[Cloudflare Stream<br/>video] RC[RevenueCat<br/>store IAP] CARD[Card MID<br/>tokenized] DL[dLocal<br/>local wallets] PP[pawaPay<br/>mobile money] VAS[adbilling.me<br/>VAS/DCB] APNS[APNs / FCM<br/>push] PH[PostHog<br/>analytics events] end
MOBILE --> API ADMIN --> API DOCS -.->|static| END(( ))
API --> PG API --> REDIS API --> CF API --> RC API --> CARD API --> DL API --> PP API --> VAS API --> APNS API --> PHThe three surfaces
Section titled “The three surfaces”- Mobile app (
apps/mobile): React Native / Expo. Users watch, pay, comment, get pushed. Talks to/v1/*public endpoints. - Admin panel (
apps/admin): Next.js 16. Operators do everything on this doc site’s guides. Talks to/v1/admin/*behind a permission guard. - Docs site (
apps/docs, this site): Astro Starlight. Static; no runtime dependencies.
Server-side
Section titled “Server-side”- API (
services/api): NestJS on Node 22. Single monolith with modular structure (auth, catalog, media, publishing, curation, users, payments, monetization, analytics, engagement, admin). - Postgres 15: main store. Prisma ORM. Migrations in
services/api/prisma/migrations. - Redis 7: feed composer cache, rate limiting, ephemeral state.
Background work
Section titled “Background work”Five @nestjs/schedule crons live inside the API process. See Cron jobs.
Provider integrations
Section titled “Provider integrations”Some are live in v2, some are stubbed. See Integrations status for the definitive list.
RBAC in the request path
Section titled “RBAC in the request path”sequenceDiagram autonumber participant U as Admin UI participant API as NestJS participant AG as AdminGuard participant PG as PermissionsGuard participant SVC as Service participant DB as Postgres
U->>API: PUT /admin/users/:id/wallet/adjust<br/>+ Authorization: Bearer <jwt> API->>AG: extract & verify JWT AG->>AG: check token_version vs DB AG-->>API: req.admin = { sub, role, permissions, ... } API->>PG: check @RequirePermission('wallet:adjust') PG->>PG: hasAnyPermission(granted, required) alt granted PG-->>API: OK API->>SVC: adjust(...) SVC->>DB: insert coin_ledger x2 + audit_log SVC-->>U: {kind:'applied', ...} else denied PG-->>U: 403 insufficient_permissions endDeployment topology (planned)
Section titled “Deployment topology (planned)”- Admin panel →
admin.twistcue.com(behind Apache + Let’s Encrypt, systemd) - API →
api.twistcue.connectorme.com(same box in v2) - Docs → static hosting (any CDN); build artifact from
apps/docs/dist/
See Deployment guide for the box layout.
Multi-tenant / market awareness
Section titled “Multi-tenant / market awareness”- The API is not multi-tenant. Single instance serves all markets.
- Market is a per-user attribute (
users.country_code) and a scope onprice_overrides,free_series_rotations,feature_flags(via naming convention). - Currency and language are resolved from the user’s locale at request time.
Data flow: an episode unlock
Section titled “Data flow: an episode unlock”sequenceDiagram participant M as Mobile participant API as API participant PR as PricingService participant WS as WalletService participant DB as Postgres
M->>API: POST /v1/episodes/:id/unlock/quote API->>PR: resolveForEpisode(id, userCountry) PR->>DB: lookup price_overrides<br/>episode → series → market PR-->>API: {coins: 30} API-->>M: {coinsRequired: 30} M->>API: POST /v1/episodes/:id/unlock API->>WS: unlock(userId, episodeId, coins) WS->>WS: check balance (bonus first, then paid) WS->>DB: insert coin_ledger x2 (spend + sink_unlock) WS->>DB: insert episode_unlocks row WS-->>API: {balanceTotal, unlockId} API-->>M: 200 unlocked