- Node 22+
- npm
- Wrangler authenticated locally
Check auth:
npx wrangler whoami- Install dependencies:
npm installnpm install also installs the repo's Git hooks through Husky.
Those hooks run:
- on commit: block commits on
mainand remind you to switch to a feature branch - on commit: staged-file Biome fixes/checks
- on push: generated-file checks,
tsc --noEmit, and tests without rewriting generated files
- Copy and fill in environment values:
cp .env.example .envAPP_URL is used to derive the default Vite server.allowedHosts entry. If you need extra hostnames during local development, set VITE_ALLOWED_HOSTS to a comma-separated list.
For deployment, use a separate file:
cp .env.deploy.example .env.deployUse:
.envfor local development and your tunnel/ngrok URL.env.deployfor Cloudflare deployment values and the deployed app URL- the GitHub Actions
APP_URLrepository secret is separate again and should use the deployed public URL, not the local tunnel URL
- Bootstrap the local D1 database:
npm run db:bootstrap:local- Start the app:
npm run devdb:bootstrap:local resets the local D1 state, applies migrations, and loads the bundled sample catalog seed.
predev runs migrations automatically, so once the sample seed is loaded the schema stays current.
- public pages
- search
- account/settings UI
- playlist management flows
To exercise Twitch login, EventSub, and bot replies, fill in the Twitch-related values in .env.
For local bot testing, set TWITCH_BOT_USERNAME in .env to your dedicated test bot account, not the production bot account. The bot OAuth callback only accepts the username configured in local env, so if you want to connect jimmy_test_bot_ locally, your local .env must also say TWITCH_BOT_USERNAME=jimmy_test_bot_.
Keep the production bot username only in production secrets or deployed env. Do not point local development at the production bot account unless you intentionally want local testing to use the live bot identity.
TWITCH_SCOPES applies to the broadcaster's main app login, not the shared bot login. It needs channel:bot so bot replies can use Twitch's bot badge path, and it needs moderator:read:chatters for the chatter-first VIP lookup flow.
Do not test bot commands against a channel that is also connected in the live app unless you intentionally want both environments to react.
There are two different failure modes:
This does not usually create duplicate chat handling.
Instead, it creates a subscription ownership conflict. Twitch treats channel.chat.message subscriptions as unique by event type plus condition, and the condition includes both the broadcaster ID and bot user ID. If local and production both try to use the same broadcaster with the same bot account, one environment can end up owning the subscription and the other can fail or appear to stop receiving chat events.
That means local testing can still interfere with production, even if both environments do not reply at the same time.
This is the more dangerous case for duplicate behavior.
Because the bot user ID is different, Twitch can allow both subscriptions at once. Then a single chat command in that broadcaster's channel can be seen by both environments:
- once by production
- once by local development
That can cause:
- duplicate bot replies in chat
- duplicated side effects if both environments act on the same command
- confusing logs where both environments appear to handle the same message
Recommended practice:
- use a separate test broadcaster/channel for local bot testing
- use a separate test bot account for local bot testing
- set local
.envTWITCH_BOT_USERNAMEto the test bot account username - do not connect a production broadcaster to local development
- keep only one active EventSub webhook subscription for a given broadcaster when you are debugging command behavior
- do not leave a local tunnel subscription active while also testing the same channel in production
localhost is enough for basic Twitch OAuth testing, but full local testing for this app works better with a public HTTPS URL because Twitch webhooks need a reachable callback target.
Install cloudflared, then authenticate:
cloudflared loginCreate a named tunnel:
cloudflared tunnel create request-bot-devCreate a DNS route:
cloudflared tunnel route dns request-bot-dev dev.example.comIf cloudflared tunnel route dns uses the wrong zone or you manage multiple domains in Cloudflare, create the DNS record manually in the correct zone instead:
- type:
CNAME - name:
dev - target:
<your-tunnel-id>.cfargotunnel.com - proxied:
On
Example:
dev.example.com -> 4ac1a27b-efe2-402a-a0ae-21ec35d61591.cfargotunnel.com
This is often the easiest fix when the hostname belongs to a different zone than the one cloudflared tries to use automatically.
Create ~/.cloudflared/config.yml on macOS/Linux, or %USERPROFILE%\.cloudflared\config.yml on Windows:
tunnel: <your-tunnel-id>
credentials-file: /home/<you>/.cloudflared/<your-tunnel-id>.json
ingress:
- hostname: dev.example.com
service: http://localhost:9000
- service: http_status:404Windows example:
tunnel: <your-tunnel-id>
credentials-file: C:\Users\<you>\.cloudflared\<your-tunnel-id>.json
ingress:
- hostname: dev.example.com
service: http://localhost:9000
- service: http_status:404Run it:
cloudflared tunnel run <your-tunnel-id>Then update:
.envAPP_URL=https://dev.example.comVITE_TWITCH_EXTENSION_API_BASE_URL=https://dev.example.comfor the standalone panel buildVITE_ALLOWED_HOSTS=dev.example.comif Vite blocks the hostnameTWITCH_EXTENSION_CLIENT_ID=<your-extension-client-id>for panel setup
- Twitch app redirect URIs
https://dev.example.com/auth/twitch/callbackhttps://dev.example.com/auth/twitch/bot/callback
Before testing chat commands through the tunnel, make sure the same broadcaster is not still actively subscribed to the production EventSub callback unless that is intentional.
As an alternative:
ngrok http 9000Use the generated HTTPS URL for:
APP_URL- Twitch redirect URI base
If the ngrok URL changes, update both .env and the Twitch app redirect URIs.
The default contributor path is:
- Commit normally.
- Let the
pre-commithook block commits onmainand run staged-file Biome fixes/checks. - Push normally.
- Let the
pre-pushhook run generated-file verification, typecheck, and tests.
If you want to run the same push-time gate manually before pushing, use:
npm run check:prepushYou usually do not need to run format, lint, test, and typecheck manually in sequence.
Run these extra commands only when they fit the change:
- Full-repo Biome pass:
npm run lintIf you want Biome's full detailed output instead of the compact summary:
npm run lint:full- Production build sanity check:
npm run build- Browser flow coverage:
npm run test:e2eIf you need the old manual verification path, run:
- Typecheck:
npm run typecheck- Tests:
npm run test- Format:
npm run format- Lint:
npm run lint- Production build:
npm run buildIf you changed browser-driven behavior or UI flows, also run:
npm run test:e2eWhy format before lint in the manual path:
- Biome lint is much less noisy after formatting first
- AI-generated edits often introduce avoidable formatting drift
- running
npm run formatfirst catches a large class of pre-commit issues cheaply
npm run deploy and npm run db:bootstrap:remote read deployment values from .env.deploy, not .env.
That keeps local tunnel settings separate from deployed app settings.
The app fails early with a message telling you to run:
npm run db:migrateIf that happens, rerun migrations and restart the dev server.
If you want to restore the default local dataset:
npm run db:bootstrap:localCI includes a test that builds a fresh SQLite database from the migration files and verifies a manual playlist insert. If a migration works only on an existing local database but not from scratch, that test should fail.
- A single-command local setup that also validates Twitch credentials and tunnel configuration.
- A container-first development workflow if contributors need a standardized environment.
- More automated local smoke checks for OAuth, EventSub, and bot reply behavior.