Self-Host FangDash on Cloudflare
Deploy your own FangDash instance to Cloudflare Workers and D1 — wire up the API, web app, and PartyKit multiplayer server with wrangler and bun.
TL;DR — FangDash runs entirely on Cloudflare (Workers + D1). You deploy three services: the
API, the web app, and the PartyKit multiplayer server. bun run ship deploys all
three at once.
FangDash is a free, open-source, MIT-licensed multiplayer runner, so you can host your own copy. Everything runs on Cloudflare's infrastructure. Here's how to deploy an instance end to end.
What You'll Need
- Node.js >= 20
- Bun (see
package.jsonfor the pinned version) - Cloudflare account (the free tier works)
- Twitch Developer application (for OAuth sign-in)
- PartyKit account (for real-time multiplayer)
Step 1 of 3 — Deploy the API
The API is a Hono app on Cloudflare Workers backed by a D1 (SQLite) database.
- Create a D1 database via the Cloudflare dashboard (Workers & Pages → D1) or the CLI:
bunx wrangler d1 create fangdash-db- Update
apps/api/wrangler.tomlwith your database ID and production domains. The[[d1_databases]]block should look like:
[[d1_databases]]
binding = "DB"
database_name = "fangdash-db"
database_id = "your-database-id-here"
migrations_dir = "drizzle/"And the production environment variables:
[env.production.vars]
ENVIRONMENT = "production"
BETTER_AUTH_URL = "https://your-api-domain.workers.dev"
WEB_URL = "https://your-web-domain.workers.dev"- Run migrations against the remote database:
bunx wrangler d1 migrations apply fangdash-db --remote- Set your secrets:
bunx wrangler secret put BETTER_AUTH_SECRET --env production
bunx wrangler secret put TWITCH_CLIENT_ID --env production
bunx wrangler secret put TWITCH_CLIENT_SECRET --env production
bunx wrangler secret put RACE_TOKEN_SECRET --env productionRACE_TOKEN_SECRET signs the short-lived HMAC token that authorizes each PartyKit connection. It
must be identical in the API and the PartyKit server (Step 3) — if the two values differ,
multiplayer clients are rejected and races never start.
Generate strong secrets with openssl rand -base64 32 — run it once for BETTER_AUTH_SECRET
and again for RACE_TOKEN_SECRET, and paste each value when prompted.
- Deploy:
bun run ship:apiStep 2 of 3 — Deploy the Web App
The web app is Next.js 16, deployed to Cloudflare Workers via the OpenNext adapter.
- Set your API URL as a build-time environment variable:
NEXT_PUBLIC_API_URL=https://your-api-domain.workers.dev bun run ship:webOr add it to apps/web/.env.local first:
NEXT_PUBLIC_API_URL=https://your-api-domain.workers.dev
NEXT_PUBLIC_PARTYKIT_HOST=fangdash.your-username.partykit.dev- Deploy:
bun run ship:webStep 3 of 3 — Deploy PartyKit
PartyKit runs the real-time multiplayer WebSocket server (the RaceServer room).
- Set your PartyKit token:
export PARTYKIT_TOKEN=your-token- Give PartyKit the same
RACE_TOKEN_SECRETyou set on the API so it can verify race connection tokens:
bunx partykit secret put RACE_TOKEN_SECRET- Deploy:
bun run ship:partyAll three services are deployed. Your FangDash instance should now be live — visit your web domain, sign in with Twitch, and start a race to verify everything works.
Deploy Everything at Once
bun run shipThis runs all three deploy commands (ship:api, ship:web, ship:party) in sequence.
Environment Variables Reference
API (apps/api)
| Variable | Type | Where set | Description |
|---|---|---|---|
BETTER_AUTH_SECRET | Secret | wrangler secret put | Long random string for signing sessions |
TWITCH_CLIENT_ID | Secret | wrangler secret put | Twitch OAuth app client ID |
TWITCH_CLIENT_SECRET | Secret | wrangler secret put | Twitch OAuth app client secret |
RACE_TOKEN_SECRET | Secret | wrangler secret put | Signs race connection tokens — must match PartyKit |
BETTER_AUTH_URL | Plaintext | wrangler.toml | Public URL of the API worker |
WEB_URL | Plaintext | wrangler.toml | Public URL of the web worker (for CORS + redirects) |
ENVIRONMENT | Plaintext | wrangler.toml | "development" or "production" |
Web (apps/web)
| Variable | Type | Where set | Description |
|---|---|---|---|
NEXT_PUBLIC_API_URL | Plaintext | .env.local / CI env | Public URL of the API worker |
NEXT_PUBLIC_PARTYKIT_HOST | Plaintext | .env.local / CI env | PartyKit host (e.g. fangdash.user.partykit.dev) |
PartyKit (apps/party)
| Variable | Type | Where set | Description |
|---|---|---|---|
PARTYKIT_TOKEN | Secret | shell env | Deploy token from your PartyKit account |
RACE_TOKEN_SECRET | Secret | partykit secret put | Verifies race connection tokens — must match the API |
Local Development
Copy the example files and fill in your values:
cp apps/api/.dev.vars.example apps/api/.dev.vars
cp apps/web/.env.example apps/web/.env.localThen run the whole stack locally with bun run dev (web on 3000, api on 8787, party on 1999, docs on 3001).
Setting Up Twitch OAuth
Twitch is the only sign-in provider (via Better Auth), so you need a Twitch application before authentication works.
Add both OAuth Redirect URLs — local development
http://localhost:8787/api/auth/callback/twitch and production
https://your-api-domain.workers.dev/api/auth/callback/twitch.
The Twitch OAuth redirect URL must match your deployed API domain exactly — including the
protocol (https://) and the path /api/auth/callback/twitch. If sign-in redirects to a blank
page or an error, this mismatch is almost always the cause.
Troubleshooting
| Error / Symptom | Cause | Fix |
|---|---|---|
| OAuth redirects to blank page | Redirect URL mismatch in Twitch dev console | Ensure the redirect URL matches your API domain exactly, including protocol and /api/auth/callback/twitch path |
| "Auth not configured" in prod | Missing Cloudflare secrets | Run all wrangler secret put commands: BETTER_AUTH_SECRET, TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET |
| D1 "table not found" | Database migrations not applied | Run bunx wrangler d1 migrations apply fangdash-db --remote |
| Race clients rejected / kicked | RACE_TOKEN_SECRET differs across services | Set the same secret on the API and PartyKit, then redeploy both |
| PartyKit connection refused | Wrong host or missing token | Verify NEXT_PUBLIC_PARTYKIT_HOST matches your PartyKit deployment URL and PARTYKIT_TOKEN is set |
| CORS errors in browser console | WEB_URL mismatch | Update WEB_URL in wrangler.toml [env.production.vars] to match your actual web domain exactly |
| Admin panel shows "Forbidden" | User role is not admin or dev | Run bun run promote-admin YourUsername --remote (see Admin docs) |
| Multiplayer rooms won't start | PartyKit not deployed | Run bun run ship:party and verify the deployment URL |
| Score submission fails silently | Rate limit hit (429) | Wait 60 seconds and retry, or check the response for a 429 status and Retry-After header |
| "FORBIDDEN" on all actions | Account is banned | Check the user's ban status in the admin panel or D1 database |
| Build fails with type errors | Dependencies out of date | Run bun install, then bun run typecheck to identify issues |
PartyKit Deployment Details
To expand on Step 3 above:
-
Get a PartyKit token: Sign up at partykit.io, open your account settings, and generate an API token.
-
Set the token:
export PARTYKIT_TOKEN=your-token-here- Match the race token secret: Give PartyKit the same
RACE_TOKEN_SECRETas the API — otherwise the party server can't verify the HMAC connection tokens the web client sends as?token=.
bunx partykit secret put RACE_TOKEN_SECRET- Deploy:
bun run ship:party- Verify deployment: After deploying, PartyKit outputs your host URL. It looks like:
fangdash.your-username.partykit.dev-
Update your web app: Set
NEXT_PUBLIC_PARTYKIT_HOSTinapps/web/.env.localto this URL (without thehttps://prefix), then redeploy the web app for the change to take effect. -
Test the connection: Open your web app, start a multiplayer race, and watch the browser console for WebSocket connection messages.
Developer Setup
Run FangDash locally — clone the repo, install with Bun, create a Cloudflare D1 database, set env vars, and start the dev server on localhost
Web App Tour: Every Page & Route
Tour every page in the FangDash web app — home, play, race, leaderboard, achievements, skins, profile, settings, and the admin panel.