Appearance
Production Container Architecture
This page documents the production runtime shape of Swagg Bet by container, not by source-code layer. The diagram follows the same left-to-right flow as the logic architecture reference: users, edge proxy, app surfaces, core API, providers, and database.
Use this page when deploying, debugging traffic, checking service ownership, or explaining which process is responsible for a failure.

Vector source: container-architecture.svg
What This Diagram Shows
The production stack has one host-level reverse proxy, application containers (Next.js surfaces, API, and workers), one database container, and several external provider systems.
- Nginx is the public edge. It terminates TLS and proxies domains to local container ports.
- Next.js containers serve user-facing surfaces.
web,admin,support, andp2pare separate deployable images. apiis the core backend runtime. It handles Hono routes, provider callbacks, money flows, and in-process background jobs.postgresis the only datastore container. The mainswaggbetdatabase and the separatep2pdatabase live in the same Postgres service.- Notifications, analytics, Customer.io, and realtime are worker containers under
apps/workerswith separate images. Analytics and Customer.io run only when their complete credential pairs are configured. Transactional email is sent in-process byapiviaEMAIL_PROVIDER(Resend, MailerSend, or UniOne). redisbacks WebSocket tickets and pub/sub fan-out for the realtime worker. The API also connects to Redis for ticket minting.- Workspace packages are build-time code, not runtime services. Packages such as
@repo/db,@repo/auth,@repo/ui, and@repo/walletare bundled into the app images that import them.
Production Traffic Paths
| Flow | Runtime path | Notes |
|---|---|---|
| Player site | Browser -> Nginx -> web:3000 -> api:8787 -> postgres:5432 | Normal casino lobby, wallet, profile, game, and VIP traffic. |
| Admin panel | Browser -> Nginx -> admin:3001 -> api:8787 and postgres:5432 | Only admin uses direct SQL among the Next.js frontends. web and support call the API only. |
| Support desk | Browser -> Nginx -> support:3002 -> api:8787 -> postgres:5432 | Support staff workspace and ticket operations. |
| P2P payment | Browser -> Nginx -> p2p:3005 -> postgres:5432/p2p -> api:8787 webhook | P2P has its own database schema and calls back into the casino API. |
| Transactional email | api:8787 → Resend, MailerSend, or UniOne | In-process via apps/api/src/services/email (EMAIL_PROVIDER). |
| In-app notifications | notifier:8790 → postgres | Campaign outbox delivery worker. |
| Customer.io CRM events | customerio:8791 → postgres → Customer.io Classic Track API | Optional; selected by complete Customer.io credentials. Run one delivery replica. |
| Analytics streaming | streamer:8789 → postgres → ClickHouse | Optional; selected by complete ClickHouse credentials. |
| Realtime messaging | realtime:8792 → postgres + redis | Claims private_state_outbox and realtime_event_outbox, publishes to Redis for WebSocket fan-out. |
| Provider callbacks | External provider -> Nginx -> api:8787 -> postgres:5432 | Game platform, payment, and webhook traffic terminates at the API. |
Container Inventory
Source of truth: docker/compose/docker-compose.yml.
| Compose service | Container | Runtime | Port | Public domain | Health check |
|---|---|---|---|---|---|
web | swaggbet-web | Next.js | 3000 | swagg.bet | /api/health |
admin | swaggbet-admin | Next.js | 3001 | admin.swagg.bet | /api/health |
support | swaggbet-support | Next.js | 3002 | support.swagg.bet | /api/health |
p2p | swaggbet-p2p | Next.js | 3005 | pay.swagg.bet | /api/internal/ensure-admin |
api | swaggbet-api | Hono | 8787 | api.swagg.bet | /health |
streamer | swaggbet-streamer | Workers analytics | 8789 | Internal only | /health |
notifier | swaggbet-notifier | Workers notifications | 8790 | Internal only | /health |
customerio | swaggbet-customerio | Workers Customer.io | 8791 | Internal only | /health |
realtime | swaggbet-realtime | Workers realtime | 8792 | Internal only | /health |
redis | swaggbet-redis | Redis 7 | 6379 | Internal only | redis-cli ping |
postgres | swaggbet-postgres | PostgreSQL 16 | 5432 | Internal only | pg_isready |
Container Responsibilities
web
Player-facing Next.js app for the casino lobby, games, wallet, VIP, settings, payment-result screens, and public player flows.
Owns: UI rendering and browser-side player experience.
Does not own: database queries or provider orchestration. It calls api through NEXT_PUBLIC_API_URL.
admin
Back-office Next.js app for operators. It manages players, withdrawals, bonuses, games, support, marketing, roles, permissions, audit logs, and campaign tools.
Owns: admin UI, permission-guarded Next.js route handlers, and audited admin mutations.
Important production detail: admin is the only Next.js frontend with direct database access. Some admin route handlers call @repo/db operations directly with DATABASE_URL. web and support do not connect to Postgres in production. Keep admin handlers thin and route database access through operations.
support
Support staff Next.js app for ticket and team workflows.
Owns: support-agent UI and staff workspace flows.
Depends on: api for support conversations, ticket state, auth, and related data.
p2p
Standalone payment-provider app. It has its own runtime surface and its own p2p database inside the same Postgres container.
Owns: P2P payment pages, provider admin credentials, receipt uploads, and payment lifecycle state.
Calls back to: api:8787/webhook/p2p when a payment changes state.
api
Core Hono backend. This is the main runtime for casino business behavior.
Owns:
- player, wallet, game, VIP, support, platform, payment, notification, and webhook routes;
- provider calls for games, payments, wallet networks, and internal P2P;
- background work such as session cleanup, bonus scheduling, game sync, exchange rates, and support maintenance.
Important production detail: there is no Celery or separate queue worker container beyond apps/workers entrypoints. Background jobs such as session cleanup run inside api. Realtime delivery is handled by the realtime worker plus Redis, not inside api.
notifier
In-app notification campaign delivery worker (apps/workers notifications entrypoint).
Owns: scheduled campaign promotion, audience enqueue, and delivery outbox processing.
customerio
Customer.io Classic Track API event delivery worker (apps/workers customerio entrypoint).
Owns: customerio_outbox claim/delivery. Prefer a single delivery replica until multi-worker claim behavior is load-tested.
streamer
Analytics worker that moves queued event data from Postgres to external ClickHouse (apps/workers analytics entrypoint).
Owns: polling, worker identity, event locking, batching, and ClickHouse inserts.
Does not own: user-facing analytics UI or product behavior. /health is liveness-only; backlog depth is exposed on /ready and metrics.
realtime
Private realtime outbox worker (apps/workers realtime entrypoint).
Owns: polling private_state_outbox (wallet balance) and realtime_event_outbox (support, withdrawal, notifications), Redis pub/sub publish, stale-lock recovery.
Depends on: postgres, redis. API WebSocket gateway subscribes to the same Redis channels.
Does not own: ticket minting or WebSocket termination (those live in api).
postgres
Primary database container.
Databases:
swaggbetfor the main casino schema and Drizzle migrations.p2p, created bydocker/postgres/init/02-create-p2p-db.sql, for the standalone payment provider.
Volumes:
postgres_datafor database durability.p2p_uploadsbelongs top2p, not Postgres, and stores uploaded payment receipt files.
Public Edge Mapping
Nginx config lives in nginx/*.conf and maps public domains to local ports.
| Domain | Upstream |
|---|---|
swagg.bet | localhost:3000 |
admin.swagg.bet | localhost:3001 |
support.swagg.bet | localhost:3002 |
pay.swagg.bet | localhost:3005 |
api.swagg.bet | localhost:8787 |
docs.swagg.bet | static VitePress files on disk |
Operational Checks
When production is unhealthy, check from the edge inward:
- Public domain and TLS: confirm the Nginx server block and certificate for the domain.
- Container health: check the container health endpoint from the inventory table.
- Internal dependency: check whether the container can reach
api,postgres,p2p, or the external provider it depends on. - Environment: verify
DATABASE_URL,REDIS_URL,NEXT_PUBLIC_API_URL, provider keys, webhook URLs, and service secrets for the affected flow. - Database: confirm whether the failing flow uses the main
swaggbetdatabase or the separatep2pdatabase.
Deployment Notes
Production images are built from the repo root:
bash
./build.sh
docker compose up -dImage build pattern:
| Runtime | Dockerfile | Build args |
|---|---|---|
| Next.js apps | docker/Dockerfile.nextjs | APP_NAME, APP_PORT, app-specific public URLs |
| Hono apps/workers | docker/Dockerfile.hono | APP_NAME |
| Shared base | docker/Dockerfile.base | workspace dependencies and build foundation |
Mermaid Source
This Mermaid version is intentionally simpler than the SVG. It is useful for quick edits, not for final presentation.
mermaid
flowchart LR
user["Players, admins, support"] --> nginx["Nginx TLS reverse proxy"]
nginx --> web["web :3000"]
nginx --> admin["admin :3001"]
nginx --> support["support :3002"]
nginx --> p2p["p2p :3005"]
nginx --> api["api :8787"]
web --> api
admin --> api
support --> api
admin -. "direct SQL (admin only)" .-> postgres["postgres :5432"]
api --> postgres
api --> p2p
p2p --> postgres
p2p -. "webhook" .-> api
notifier["notifier :8790"] --> postgres
customerio["customerio :8791"] --> postgres
streamer["streamer :8789"] --> postgres
realtime["realtime :8792"] --> postgres
realtime --> redis["redis :6379"]
api --> redis
api --> providers["game, payment, wallet providers"]
api --> email["Resend, MailerSend, or UniOne"]
customerio --> cio["Customer.io Track API"]
streamer --> clickhouse["ClickHouse"]Rule of Thumb
If the code is shared behavior, put it in packages/*. If it needs its own deployment boundary, health check, persistent volume, or blast-radius isolation, make it a container. Most new casino product logic should go into api or a shared package, not a new service.