Skip to content

Architecture overview

The TwistCue Admin is one of three surfaces on top of the same NestJS API and Postgres database.

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 --> PH
  • 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.
  • 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.

Five @nestjs/schedule crons live inside the API process. See Cron jobs.

Some are live in v2, some are stubbed. See Integrations status for the definitive list.

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
end
  • 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.

  • The API is not multi-tenant. Single instance serves all markets.
  • Market is a per-user attribute (users.country_code) and a scope on price_overrides, free_series_rotations, feature_flags (via naming convention).
  • Currency and language are resolved from the user’s locale at request time.
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