Skip to content

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.

AppWhat it is
apps/webThe player-facing site — lobby, wallet, games, VIP, settings
apps/adminThe back office where operators manage the platform
apps/supportA focused workspace for support agents
apps/apiA Hono API that handles money, game sessions, and webhooks
apps/workersIndependently deployable entrypoints: realtime outbox → Redis (8792, default), analytics → ClickHouse (8789), notification delivery (8790), Customer.io Classic Track outbox (8791)
apps/docsThis 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/ops for 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/client

The 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]):

  1. Register the path in apps/web/middleware.ts existence gate (matcher + probe).
  2. Add a cheap API …/exists endpoint that returns 404 for unknown IDs.
  3. On definitive miss, middleware rewrites to /not-found-gate, which calls notFound() with no loading boundary above it — real HTTP 404 + the branded root not-found UI.

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 (default responseMode: "data") → unwrapped T
  • Raw adminApi(path) → full envelope; read response.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 seeded superadmin role 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: