Architecture & Deployment
The processes
Six things run (the ingester only when asked for):
| Process | Job |
|---|---|
| API | Serves the player site and backoffice, and receives every provider callback and webhook |
| Worker | Runs all scheduled jobs |
| Ingester | Consumes the live sports feed and hosts the polling odds feed, isolated so feed problems cannot take the site down. Off by default in the deployment: it starts only when the ingester Compose profile is requested, so a plain deploy leaves it stopped |
| Player site | A single-page application |
| Backoffice | A separate single-page application on its own domain |
| Partner portal | The affiliates' single-page application, on its own domain (see Affiliate Program) |
Behind them: PostgreSQL as the system of record, and Redis for the job queue and catalog caching. Redis is a cache, not a dependency for correctness — if it is unavailable, reads fall through to the database and the site stays up, degraded rather than down.
ClickHouse is an optional third store. It holds a read-only copy of the ledger, sports bets, casino rounds, sports activity and affiliate report data for analytics, plus a 120-day recording of the raw LSports feed (so a match can be replayed as a demonstration feed), and background jobs keep it within about a minute of the main database. It never takes a write that matters: every figure that moves money comes from PostgreSQL. When ClickHouse is configured and up to date, dashboards and reports read from it. If it falls more than five minutes behind or fails to answer, those reads go back to PostgreSQL. The exceptions are the backoffice analytics charts, Pulse, activity heatmap, popularity pages and game analyzer, and the ranking console's impression figures, and those exist only on the copy. Without ClickHouse, those pages show an "unavailable" state and everything else works as before. A nightly job compares a week of daily ledger totals between the two stores and repairs any day that differs.
Database connections. Each of the three server processes has its own connection pool, and its size is configurable. Idle connections close after 30 seconds. A transaction left idle for 60 seconds is ended, so a stalled external call cannot hold locks indefinitely. There is deliberately no global query timeout: a slow money write must finish rather than be cancelled. Reporting queries set their own timeout.
Stack
TypeScript throughout, running on Bun. PostgreSQL accessed through a typed query builder, with database types generated from the live schema rather than hand-maintained. Validation schemas sit at every boundary — no raw database row reaches a route handler unvalidated.
The player site and backoffice are React single-page applications, sharing types directly with the API, so a change to an API response is a compile error in the frontend rather than a runtime surprise.
Testing
About 1,500 test files: roughly 850 on the API, and the rest across the player site, backoffice and partner portal. The density is deliberately highest where money moves: settlement alone has twelve test files covering atomicity, bonus state, cash movement, free spins, provenance and the win gate.
Continuous integration runs type checking, code style, the full API suite against real PostgreSQL and Redis containers, frontend test suites, frontend builds, and a from-scratch migration apply on every change.
Configuration
Configuration is validated at startup against a schema, and the process refuses to boot on a dangerous combination rather than starting and failing later. It will not start if, for example:
- Mail is set to print messages into the log while running in production
- The proxy configuration would collapse every visitor into a single rate-limiting bucket
- A provider secret is still a placeholder in a production environment
- A development escape hatch is enabled in a production environment
There are two independent environment switches, and the separation is deliberate: the runtime mode says "this is a real deployment" (turning on hardening such as strict transport security and error tracking), while the application environment (local, staging or production) says "the money here is real". The application environment alone decides every development shortcut: the dev-deposit and verification hatches, the mock payment provider, the match simulator and skipping email verification are available only where the money is fake. It defaults to production, so an unset value fails closed. That lets you run a fully hardened staging environment that can still seed test balances.
Deployment — the honest picture
This is the weakest area of the platform, and you should price it in.
What exists: a single server running everything under Docker Compose, with
automatic TLS, proxied DNS, and metrics pushed to a hosted monitoring service.
The database can be a managed PostgreSQL service or a bundled container. The
staging server lives under the roll.game domain, and its transactional mail
goes through Mailgun. It works, and there is thorough documentation of exactly
what a deploy needs. The staging server tracks a single branch: a deploy is a
fast-forward pull of that branch on the server, a rebuild, the migrations, and
then starting the containers, in that order. Files are never copied onto the
server by hand.
A setup wizard for a fresh server. make setup, run from a clone on the
server, asks for staging or production, the three hostnames (player site,
backoffice, partner portal), the certificate email, bundled or external
Postgres, mail, local disk or S3 storage and, on staging only, the mock payment
provider. It then writes the environment files with every secret generated and
every origin derived from the hostnames, so the result passes the startup
checks. It downloads the IP-to-country database, builds, migrates, seeds,
starts the stack and checks each host over HTTPS. Re-running is safe: existing
values are never changed. It prints the first backoffice admin password once.
The wizard sets a server up; it is not a deployment pipeline, and metrics,
the crypto service, ClickHouse, game providers, Google sign-in and bundled
database backups are left as printed next steps. ClickHouse can be added on the same server as an
opt-in service that a normal deploy never starts or restarts. It is tuned to
fit the current small staging server (2 GB RAM, 1 vCPU), but it pushes the
other containers into swap. A real deployment should size a larger server or
host the analytics store separately.
What does not exist:
- No infrastructure as code. The wizard configures a server you have already provisioned; it does not create one, DNS, firewall or monitoring.
- No continuous deployment. Deploying is a manual pull and rebuild on the host, with migrations run by hand before the new containers start. Nothing in CI builds or publishes an image.
- No rollback path.
- No production environment. What runs today is a staging server holding no real money.
- No secret management. Application secrets live in a hand-edited file on the host, not in a secrets manager.
- No database backup policy of your own beyond the managed provider's defaults, no configured retention, no encryption-at-rest posture, and no rehearsed restore. Nothing backs up the Redis volume holding queue state.
- No network isolation — the database is reachable from outside.
- No load testing. Sportsbook traffic is spiky by nature (kickoff, in-play goals) and behaviour at peak is unknown.
- No runbooks for the failure modes this system actually has: feed outage, stuck rounds, settlement disputes, provider callback storms.
Also note: the hosting region is undecided, and it must be settled against your licensing and data-residency obligations before go-live rather than after.
Observability
Metrics, dashboards and alerting are genuinely in place: more than 50 application metrics across the three processes, four dashboards (service health, money integrity, sports ingest and the ClickHouse analytics copy), twenty alert rules, health and readiness probes that distinguish "starting up" from "broken", and error tracking that strips personal data before it leaves your servers. When ClickHouse runs, its own server metrics (memory, disk, merges, failed queries) are also collected. Alerts fire if a sync stops succeeding, if the nightly reconcile fails or if reads start erroring.
Structured logging. The servers log through a structured logger: one coloured line per message when run locally, one JSON line per message on staging and production. Each line carries a module name and queryable fields (a round, bet or event id) rather than text built into the message; secrets, tokens and personal data are stripped before a line is written, and a test fails any bare console output. Job run logs in the backoffice show these lines. Tracing a single disputed bet across ingest, placement and settlement means searching these logs for its durable id. A request identifier is generated and returned on every response, but no log line carries it yet, and there is no distributed tracing across the API, worker and ingester by design.
Scope & Roadmap
Implemented. The multi-process architecture; a make setup wizard that takes a
fresh server from clone to a running site; validated fail-fast
configuration; generated database types; bounded database connection pools;
an optional ClickHouse analytics copy with automatic fallback, nightly
reconciliation and its own monitoring; about 1,500 test files with CI
running the full suite against real infrastructure; structured logging, metrics, dashboards,
alerting, health probes and error tracking; a documented working deployment.
Not built. Everything in the deployment list above. Budget for a platform-engineering effort — infrastructure as code, a deployment pipeline, secret management, backup and restore, network isolation, load testing and runbooks — as a distinct workstream from the product itself.