A provably-fair social/sweepstakes slot β play Β· earn Β· progress Β· redeem
A minimal but production-shaped casino proof of concept: one polished slot machine with verifiable fair outcomes, a dual virtual-currency economy, VIP progression, daily login bonuses, and redemption tracking β wired end-to-end across web, API, and database.
β οΈ Not a real-money product. No payments, no purchases, no KYC. Sweeps redemptions are tracked through a status lifecycle, never paid out.
- For players β What it is Β· How to play Β· Features
- For developers β Quick start Β· Architecture Β· Tech stack Β· Make targets Β· Provably-fair spins Β· Money & safety Β· Testing Β· CI Β· Configuration Β· Docs
Lunaland is a social / sweepstakes casino: you play real casino-style games with virtual currency β no purchase, ever. Wins in the promotional currency (Sweeps Coins) can build up to a redemption. This POC proves the whole engagement loop β play β earn β progress β redeem β around one polished slot game with provably-fair outcomes.
- Register β you start with 10,000 Luna Coins π + 2.00 Sweeps Coins π at Bronze tier.
- Spin β pick a currency and a bet, then spin a 3Γ3 machine with 5 paylines. Wins update your balance and earn XP.
- Come back daily β claim a daily bonus; consecutive days grow your streak for bigger rewards.
- Climb the ranks β wagering earns XP and moves you up the VIP tiers, unlocking larger bonuses.
- Redeem β once you hold β₯ 50.00 Sweeps Coins, request a redemption (gift card / bank / PayPal) and track its status.
| π° Provably-fair slots | Classic 3Γ3 machine, 5 paylines (3 lines + 2 diagonals), 6 symbols + a Luna scatter. Every spin is server-computed and independently verifiable. Target RTP β 94%. |
| π π Dual currency | Luna Coins for free play and Sweeps Coins as the redeemable promotional currency (1 SC β $1, redeemable portion only). Exact integer accounting β never floats. |
| π VIP progression | Earn XP by wagering and climb Bronze β Silver β Gold β Platinum β Diamond. Higher tiers grant larger daily bonuses and an XP boost. |
| π Daily login bonus | One claim per day of Luna + Sweeps, scaled by tier and a consecutive-day streak. Idempotent β re-claims are cleanly rejected. |
| πΈ Redemption tracking | At β₯ 50 SC redeemable, submit a redemption; Sweeps are held and the request enters the PENDING β APPROVED/REJECTED β PAID lifecycle. |
cp .env.example .env # or: make env
make up # build + start db, api, web (with hot reload)| Service | URL | Notes |
|---|---|---|
| Web (Next.js) | http://localhost:3000 | the app |
| API (NestJS) | http://localhost:4000 | health check at /health |
| Postgres | localhost:5432 |
migrated + seeded on first boot |
Requirements: Docker + Docker Compose.
make upis the supported path; Node is pinned to 24.16.0 (.nvmrc) for any host-side tooling.
The browser only ever calls the same-origin /api/*; the Next.js server proxies those
requests to the NestJS API (API_PROXY_TARGET). No CORS, and no per-environment URL baked
into the client β the same image serves make up and make test.
flowchart LR
UI["Browser Β· React UI"]
Next["Next.js web :3000<br/>App Router + /api proxy"]
Nest["NestJS api :4000<br/>auth Β· wallet Β· slot<br/>vip Β· bonus Β· redemption"]
PG[("Postgres 16 :5432<br/>Drizzle ORM")]
Shared[["@casino/shared<br/>BEβFE type contract"]]
UI -- "same-origin /api/*" --> Next
Next -- "server-side proxy" --> Nest
Nest -- "SQL" --> PG
Shared -. "types" .-> Next
Shared -. "types" .-> Nest
A contract-first monorepo (see ADR-0007): both
apps build against the frozen @casino/shared types β plus docs/api-contract.md and
docs/db-schema.md β so the tracks below develop in parallel against disjoint directories.
.
βββ backend/ # NestJS API β auth Β· wallet Β· slot Β· vip Β· bonus Β· redemption
β βββ src/db/ # Drizzle schema, migrations, seed
βββ frontend/ # Next.js App Router UI (Tailwind v4 "Luna" theme) + /api proxy
βββ packages/shared/ # @casino/shared β the BEβFE type contract
βββ docs/ # PRD, ADRs (0001β0008), API & DB contracts, CI notes
βββ infra/ # Postgres init scripts
βββ tests/ # Playwright integration + e2e (run against the containers)
βββ docker-compose*.yml # base Β· override (dev) Β· test Β· ci
βββ Makefile # task runner (see below)
| Layer | Tech |
|---|---|
| Frontend | Next.js 16 (App Router) Β· React 19 Β· Tailwind CSS v4 Β· TypeScript |
| Backend | NestJS 11 Β· TypeScript |
| Data | PostgreSQL 16 Β· Drizzle ORM |
| Shared | @casino/shared workspace package (the BEβFE type contract) |
| Infra | Docker Compose Β· Makefile Β· Node 24.16 |
| Tests | Jest (unit) Β· Playwright (integration + e2e, run against the containers) |
| Target | What it does |
|---|---|
make up |
Build + start db/api/web with hot reload |
make down Β· make clean |
Stop Β· stop and wipe the db volume |
make logs Β· make ps |
Tail logs Β· list containers |
make migrate Β· make seed |
Apply Drizzle migrations Β· seed VIP tiers + demo user |
make psql |
Open a psql shell on the db |
make test Β· make test-be |
Full containerized suite Β· backend unit tests |
make lint Β· make typecheck |
Lint workspaces Β· TypeScript check |
make audit |
Fail on high/critical CVEs |
make help |
List every target |
Every spin is server-authoritative and verifiable. A keyed byte stream β
HMAC_SHA256(serverSeed, "<clientSeed>:<nonce>") β drives weighted reel strips via
rejection sampling (no modulo bias). Each spin stores its seeds, so the grid can be
recomputed and checked after the fact. The engine targets RTP β 94%, asserted by a
200k-spin statistical test. See ADR-0004.
Balances are integer minor units (no floats). Every change runs inside a DB transaction with row locking and appends to an immutable ledger, so balances always reconcile and never go negative. Security basics: bcrypt password hashing, JWT auth, request validation, rate limiting, Helmet headers, a CORS allowlist, parameterized queries, and no secrets in source.
make test # boots fresh db/api/web containers and runs the full suite against them
make test-be # backend unit tests in-process (engine determinism + 200k-spin RTP)make test brings up an ephemeral stack (docker-compose.test.yml) and runs a Playwright
runner against the containers: API integration specs hit the api container, and a browser
e2e drives the full journey β register β spin β daily bonus β redemption gating β error path β
on the web container.
GitHub Actions (.github/workflows/ci.yml) runs on every push and
pull request in two stages:
staticβnpm ci, build shared β backend β frontend, then lint, typecheck, the backend Jest suite (engine determinism + RTP), and annpm auditgate.e2eβ builds the images and runs the containerized Playwright suite (docker compose β¦ upagainstdb/api/web), uploading the Playwright report as an artifact.
Both npm and Docker layers are cached (GitHub Actions cache) to keep runs fast. Details in
docs/ci.md.
All configuration is via environment variables β copy .env.example to .env.
Never commit .env.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Postgres connection string used by the API |
POSTGRES_USER Β· POSTGRES_PASSWORD Β· POSTGRES_DB |
Database credentials |
JWT_SECRET |
Auth signing key β generate with openssl rand -hex 32 |
JWT_EXPIRES_IN Β· BCRYPT_ROUNDS |
Token lifetime Β· password-hashing cost |
WEB_ORIGIN |
CORS allowlist (the web origin permitted to call the API) |
API_PROXY_TARGET |
Where Next proxies /api/* server-side (the API address) |
THROTTLE_TTL Β· THROTTLE_LIMIT |
Rate-limit window Β· max requests per window |
API_PORT Β· WEB_PORT Β· POSTGRES_PORT |
Service ports (4000 Β· 3000 Β· 5432) |
The backend validates required vars at boot (DATABASE_URL, JWT_SECRET) and fails fast with a
clear message if any are missing.
- Product:
docs/prd/PRD-current.md - Architecture decisions:
docs/adr/(ADR 0001β0008) - API contract (BEβFE):
docs/api-contract.md - DB schema (DBβBE):
docs/db-schema.md - CI:
docs/ci.md