Appearance
What is Swagg Bet
Swagg Bet is a crypto-first online casino. Players sign up, fund a wallet with crypto or fiat, play slots and live games sourced from third-party providers, and climb a VIP ladder that pays back a share of their wagering. Behind the scenes, an operations team manages players, promotions, payouts, and marketing through a separate admin panel.
The whole thing lives in a single pnpm + Turborepo monorepo, so the player site, the admin panel, the API, and the shared business logic are versioned and built together.
The moving parts
There are six apps in the monorepo plus a workers package, and a set of shared packages.
| App | What it is |
|---|---|
apps/web | The player-facing site — lobby, wallet, games, VIP, settings |
apps/admin | The back office where operators manage the platform |
apps/support | A focused workspace for support agents |
apps/api | A Hono API that handles money, game sessions, and webhooks |
apps/workers | Independently deployable entrypoints: realtime outbox → Redis (8792, default), analytics → ClickHouse (8789), notification delivery (8790), Customer.io Classic Track outbox (8791) |
apps/docs | This VitePress documentation site |
Transactional email (password reset, verification, contact-us) is sent in-process by apps/api.
Most product logic does not live in the apps themselves. It sits in shared packages so the web app, the admin panel, and the API all behave consistently:
packages/db— the database schema and every query, grouped by domain (users, wallet, segments, support, …).packages/wallet— balances, deposits, registration side effects, and segment re-evaluation hooks.packages/shared— types, currency list, fallback game catalog, Customer.io contracts, observability (@repo/shared/observability/*), and segment triggers.packages/ui— player UI plus ops primitives under@repo/ui/opsfor admin/P2P.packages/auth— NextAuth session handling and the unified API token.packages/client— HTTP transport (@repo/client/http) and React Query domain hooks.packages/config— ESLint, TypeScript, tsup, and Vitest presets.
How a request flows
The player site rarely touches the database directly. It calls the API, which goes through the shared operations and wallet packages:
apps/web → apps/api → packages/db (operations)
→ packages/wallet (balances)
apps/web → packages/ui, packages/clientThe admin panel is a little different. It is a Next.js app that can reach the database directly through packages/db operations, while still funneling sensitive actions (blocking a player, adjusting a balance) through audited operations rather than ad-hoc SQL.
Public dynamic routes and HTTP 404
Next.js App Router streams loading.tsx Suspense shells with status 200 before the page resolves. Calling notFound() inside a page that sits under a loading.tsx therefore cannot change the response status — crawlers and curl see 200 and an empty shell.
For public, indexable dynamic player routes (/slots/[slug], /providers/[providerId]):
- Register the path in
apps/web/middleware.tsexistence gate (matcher + probe). - Add a cheap API
…/existsendpoint that returns 404 for unknown IDs. - On definitive miss, middleware rewrites to
/not-found-gate, which callsnotFound()with no loading boundary above it — real HTTP 404 + the branded rootnot-foundUI.
Do not rely on page-level notFound() alone for SEO/status on these routes. Fail open on API errors so a blip does not 404 every real game.
Admin API response shapes
Admin list and detail endpoints use two envelopes. Mixing them up produces empty UI with HTTP 200 (silent failure).
Paginated lists — flat data
paginatedJson({ data: T[], page, limit, total }) puts the array in data itself:
ts
// Correct
const json = await adminApi<{ data?: Game[] }>("/api/admin/games?…");
const games = Array.isArray(json.data) ? json.data : [];
// Wrong — nested collection key does not exist
const games = json.data?.games ?? [];useAdminQuery({ allPages: true }) / adminApiAllPagesData already unwrap to T[]. Prefer those for pickers that need the full catalog.
Single resources — { data: T }
adminJsonData(payload) wraps as { data: payload }. Clients must unwrap:
adminApiData(path)/useAdminQuery(defaultresponseMode: "data") → unwrappedT- Raw
adminApi(path)→ full envelope; readresponse.data, never invent top-level keys
Example: GET /api/admin/fx-rates returns { data: { rates } }. Reading response.rates from adminApi silently yields empty rates and identity FX conversion.
Authentication in one sentence
Sign-in uses NextAuth with a JWT session. That session carries a unified token the browser attaches to every API call, so the same identity works across the web app and the Hono API even though they run on different origins.
Two kinds of "staff"
It is worth separating two ideas early, because they are easy to confuse:
- Admins are back-office users with role-based permissions (
players.view,payments.manage, and so on). Roles live in the database, and a seededsuperadminrole bypasses every check. - Support staff are a separate kind of account — agents and managers who only work tickets. They are not the same as admins.
Where features are documented
The rest of these docs are split the same way the product is:
- Player experience covers what a customer can see and do — accounts, wallet & payments, games, rewards, and tips, alerts & support.
- Back office covers the operator tools — the dashboard and access model, players, segments, promotions, the games catalog, withdrawals, and marketing.