System Overview
PlayPalz is a set of small, single-purpose services around one PostgreSQL database and one Redis instance, serving a React Native client. There is no service mesh, no event bus, and no CQRS — the services talk to each other over plain HTTP and Redis, and every one of them reads the same database through the same Prisma client.
That simplicity is deliberate and worth preserving.
The whole system
The services
| Service | Package | Port | Kind | Owns |
|---|---|---|---|---|
| api | @playpals/api | 4000 | HTTP | Every REST endpoint; the only writer for most domains |
| esu | @playpals/esu | 4010 | WebSocket | Socket.IO connections, presence, typing, live fan-out |
| ogun | @playpals/ogun | — | Worker | Image variants and Mux video asset creation |
| anansi | @playpals/anansi | 4005 | Cron + HTTP | Creator earnings, ledger, Stripe payouts |
| igdb-heartbeat | @playpals/igdb-heartbeat | — | Cron | Syncs the game catalog from IGDB |
| web | @playpals/web | 3000 | HTTP | Public marketing site |
| mobile | @playpals/mobile | — | Client | The product |
Design principles you should follow
One database, one client. Every service imports @playpals/db. There is no per-service schema and no service-to-service data API for reads. Adding a service means adding another consumer of the same Prisma client.
The API is the write path. esu deliberately does not write messages — the client POSTs to the API, the API persists and then asks esu to fan out. That keeps persistence and authorization in one place and makes the realtime layer stateless and disposable.
esu does not do authorization itself. It calls back into the API (/api/v1/dm/:id/authz, /api/v1/room/:id/authz) on every join. One implementation of the access rules, not two.
Heavy work is queued. Image resizing and video transcoding never happen in a request. The API enqueues to BullMQ and returns immediately; ogun does the work and pushes a realtime update when it finishes.
Redis is three things. BullMQ queue backend, Socket.IO adapter pub/sub (so any pod can reach any connection), and a response cache for the expensive discovery and trending queries.
Request paths at a glance
| What the user does | Path through the system |
|---|---|
| Logs in | Mobile → API → Postgres → JWT back to the client |
| Opens the feed | Mobile → API → Redis cache, falling through to Postgres |
| Posts a photo | Mobile → API → Spaces + Postgres → BullMQ → ogun → Spaces → Postgres → esu → Mobile |
| Sends a DM | Mobile → API → Postgres → esu /admin → Socket.IO room → recipient |
| Joins a voice room | Mobile → API (LiveKit token) → LiveKit droplet over WebRTC |
| Goes live | Mobile → API (Mux stream credentials) → RTMP to Mux → viewers over HLS |
| Subscribes to a creator | Mobile → RevenueCat IAP → webhook → API → Postgres |
| Gets paid | anansi cron → Postgres ledger → Stripe transfer |
Each of these is expanded on its own page: Authentication, Realtime, Media Pipeline, Livestreaming & Voice, Monetization.
What is deliberately absent
- No API gateway. nginx-ingress routes by hostname; there is no aggregation layer.
- No message broker. Redis pub/sub via the Socket.IO adapter is the only asynchronous fan-out.
- No GraphQL. REST with domain-grouped routers under
/api/v1. - No server-side rendering of app content. The Next.js app is a marketing surface; the product is the mobile client.
- No CI/CD pipeline. Images are built and pushed from a developer machine with
scripts/build-*.sh. See Deploying.
