Skip to content
FangDash
Getting Started
Self-hosting

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.

Estimated time: about 20 minutes

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.json for 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.

  1. Create a D1 database via the Cloudflare dashboard (Workers & Pages → D1) or the CLI:
bunx wrangler d1 create fangdash-db
  1. Update apps/api/wrangler.toml with 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"
  1. Run migrations against the remote database:
bunx wrangler d1 migrations apply fangdash-db --remote
  1. 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 production

RACE_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.

  1. Deploy:
bun run ship:api

Step 2 of 3 — Deploy the Web App

The web app is Next.js 16, deployed to Cloudflare Workers via the OpenNext adapter.

  1. Set your API URL as a build-time environment variable:
NEXT_PUBLIC_API_URL=https://your-api-domain.workers.dev bun run ship:web

Or 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
  1. Deploy:
bun run ship:web

Step 3 of 3 — Deploy PartyKit

PartyKit runs the real-time multiplayer WebSocket server (the RaceServer room).

  1. Set your PartyKit token:
export PARTYKIT_TOKEN=your-token
  1. Give PartyKit the same RACE_TOKEN_SECRET you set on the API so it can verify race connection tokens:
bunx partykit secret put RACE_TOKEN_SECRET
  1. Deploy:
bun run ship:party

All 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 ship

This runs all three deploy commands (ship:api, ship:web, ship:party) in sequence.

Environment Variables Reference

API (apps/api)

VariableTypeWhere setDescription
BETTER_AUTH_SECRETSecretwrangler secret putLong random string for signing sessions
TWITCH_CLIENT_IDSecretwrangler secret putTwitch OAuth app client ID
TWITCH_CLIENT_SECRETSecretwrangler secret putTwitch OAuth app client secret
RACE_TOKEN_SECRETSecretwrangler secret putSigns race connection tokens — must match PartyKit
BETTER_AUTH_URLPlaintextwrangler.tomlPublic URL of the API worker
WEB_URLPlaintextwrangler.tomlPublic URL of the web worker (for CORS + redirects)
ENVIRONMENTPlaintextwrangler.toml"development" or "production"

Web (apps/web)

VariableTypeWhere setDescription
NEXT_PUBLIC_API_URLPlaintext.env.local / CI envPublic URL of the API worker
NEXT_PUBLIC_PARTYKIT_HOSTPlaintext.env.local / CI envPartyKit host (e.g. fangdash.user.partykit.dev)

PartyKit (apps/party)

VariableTypeWhere setDescription
PARTYKIT_TOKENSecretshell envDeploy token from your PartyKit account
RACE_TOKEN_SECRETSecretpartykit secret putVerifies 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.local

Then 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.

Create a new application.

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.

Copy the Client ID and Client Secret into your API secrets.

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 / SymptomCauseFix
OAuth redirects to blank pageRedirect URL mismatch in Twitch dev consoleEnsure the redirect URL matches your API domain exactly, including protocol and /api/auth/callback/twitch path
"Auth not configured" in prodMissing Cloudflare secretsRun all wrangler secret put commands: BETTER_AUTH_SECRET, TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET
D1 "table not found"Database migrations not appliedRun bunx wrangler d1 migrations apply fangdash-db --remote
Race clients rejected / kickedRACE_TOKEN_SECRET differs across servicesSet the same secret on the API and PartyKit, then redeploy both
PartyKit connection refusedWrong host or missing tokenVerify NEXT_PUBLIC_PARTYKIT_HOST matches your PartyKit deployment URL and PARTYKIT_TOKEN is set
CORS errors in browser consoleWEB_URL mismatchUpdate 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 devRun bun run promote-admin YourUsername --remote (see Admin docs)
Multiplayer rooms won't startPartyKit not deployedRun bun run ship:party and verify the deployment URL
Score submission fails silentlyRate limit hit (429)Wait 60 seconds and retry, or check the response for a 429 status and Retry-After header
"FORBIDDEN" on all actionsAccount is bannedCheck the user's ban status in the admin panel or D1 database
Build fails with type errorsDependencies out of dateRun bun install, then bun run typecheck to identify issues

PartyKit Deployment Details

To expand on Step 3 above:

  1. Get a PartyKit token: Sign up at partykit.io, open your account settings, and generate an API token.

  2. Set the token:

export PARTYKIT_TOKEN=your-token-here
  1. Match the race token secret: Give PartyKit the same RACE_TOKEN_SECRET as 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
  1. Deploy:
bun run ship:party
  1. Verify deployment: After deploying, PartyKit outputs your host URL. It looks like:
fangdash.your-username.partykit.dev
  1. Update your web app: Set NEXT_PUBLIC_PARTYKIT_HOST in apps/web/.env.local to this URL (without the https:// prefix), then redeploy the web app for the change to take effect.

  2. Test the connection: Open your web app, start a multiplayer race, and watch the browser console for WebSocket connection messages.

On this page