Stagetimer is a browser-based presentation timer with one controller and many displays, synchronized in real time.
- Frontend: Next.js app (routes: /, /control, /display)
- Backend: Express + WebSocket server in server/server.js
- Deployment model:
- Frontend on Netlify (static export)
- Backend on Heroku (Docker container)
This split is required because the app needs a persistent WebSocket backend.
"Support the Creator" button (home page) uses a Lemon Squeezy checkout overlay (lemon.js). Backend exposes POST /api/lemon/webhook which verifies the X-Signature HMAC-SHA256 header against LEMON_SQUEEZY_WEBHOOK_SECRET and tallies order_created/order_refunded events in memory (GET /api/donations).
- Next.js 15
- React 19
- TypeScript
- Tailwind CSS v4
- Express + ws
- npm (single package manager — only
package-lock.jsonis tracked)
Prerequisites:
- Node.js 20+
- npm
- Free ports: 3000 (frontend) and 8787 (backend)
Install dependencies:
npm installRun frontend and backend together:
npm run dev:allOpen:
Do not upload .env files to Netlify or Heroku. Set variables in each platform dashboard (or via CLI).
Use .env.example as reference values.
Frontend variables (Netlify, Production context):
- NEXT_PUBLIC_API_URL=https://your-backend.herokuapp.com
- NEXT_PUBLIC_WS_URL=wss://your-backend.herokuapp.com/ws
- NEXT_PUBLIC_LEMON_SQUEEZY_CHECKOUT_URL=https://your-store.lemonsqueezy.com/checkout/buy/VARIANT_ID?embed=1
Backend variables (Heroku):
- NODE_ENV=production
- PUBLIC_ORIGIN=https://your-site.netlify.app
- CORS_ALLOW_ALL=0
- SESSION_TTL_MINUTES=120
- SESSION_CODE_ALPHABET=23456789ABCDEFGHJKMNPQRSTUVWXYZ
- LEMON_SQUEEZY_WEBHOOK_SECRET=
- PORT is injected automatically by Heroku
Prerequisites: Heroku CLI logged in (heroku login), Docker available.
- Create the app (new apps get a hashed
https://<name>-<hash>.herokuapp.comURL — use that URL everywhere):heroku create stage-timer-backend --region us
- Switch to the container stack:
heroku stack:set container --app stage-timer-backend
- Set backend environment variables (from the list above).
- Build and push the Docker image from server/:
cd server && heroku container:push web --app stage-timer-backend
- Release:
heroku container:release web --app stage-timer-backend
- Verify health endpoint:
curl https://your-app-<hash>.herokuapp.com/api/healthExpected response:
{"ok":true}Notes:
- The Docker daemon's containerd image store breaks
heroku container:pushwitherror from registry: unsupported. Workaround:docker savethe image, then push withcrane push(go-containerregistry) using the Heroku registry credentials from~/.docker/config.json, thenheroku container:release web. - The server is single-dyno / demo-oriented by design: sessions, donation totals, and webhook dedup state live in memory and reset on every restart. Do not scale to multiple dynos without moving this state to shared durable storage. Eco dynos sleep after ~30 minutes of inactivity, which also wakes the in-memory state fresh.
- The server sends a WebSocket ping every 30s so Heroku's router does not idle-drop connections.
- Webhooks registered in Lemon Squeezy must point to the hashed app URL, e.g.
https://stage-timer-backend-9a2d3f8dcbec.herokuapp.com/api/lemon/webhook.
- Create a Netlify site from this GitHub repository.
- Build settings:
- Build command: npm run build
- Publish directory: out
- Node version: 20
- Add Netlify environment variable:
- NETLIFY_NEXT_PLUGIN_SKIP=true
- Add frontend environment variables in Netlify Production context:
- NEXT_PUBLIC_API_URL=https://your-backend-.herokuapp.com
- NEXT_PUBLIC_WS_URL=wss://your-backend-.herokuapp.com/ws
- NEXT_PUBLIC_LEMON_SQUEEZY_CHECKOUT_URL=https://your-store.lemonsqueezy.com/checkout/buy/VARIANT_ID?embed=1
- Trigger a production deploy.
- Update Heroku backend variable:
- PUBLIC_ORIGIN=https://your-final-site.netlify.app
- Redeploy backend service.
- Redeploy frontend so all values are in sync.
- Open frontend home page.
- Create a controller session.
- Join display with session code.
- Test start, pause, resume, reset, +30s, -30s, and end session.
- Test the "Support the Creator" overlay checkout (use Lemon Squeezy test mode first).
Netlify:
npx netlify login
npx netlify init
npx netlify env:set NETLIFY_NEXT_PLUGIN_SKIP true --context production
npx netlify env:set NEXT_PUBLIC_API_URL https://your-backend-<hash>.herokuapp.com --context production
npx netlify env:set NEXT_PUBLIC_WS_URL wss://your-backend-<hash>.herokuapp.com/ws --context production
npx netlify env:set NEXT_PUBLIC_LEMON_SQUEEZY_CHECKOUT_URL https://your-store.lemonsqueezy.com/checkout/buy/<id>?embed=1 --context production
NEXT_PUBLIC_API_URL=... NEXT_PUBLIC_WS_URL=... NEXT_PUBLIC_LEMON_SQUEEZY_CHECKOUT_URL=... npm run build
npx netlify deploy --prod --no-build --dir outHeroku:
heroku create stage-timer-backend --region us
heroku stack:set container --app stage-timer-backend
heroku config:set NODE_ENV=production PUBLIC_ORIGIN=https://your-site.netlify.app CORS_ALLOW_ALL=0 SESSION_TTL_MINUTES=120 SESSION_CODE_ALPHABET=23456789ABCDEFGHJKMNPQRSTUVWXYZ --app stage-timer-backend
heroku config:set LEMON_SQUEEZY_WEBHOOK_SECRET --prompt --app stage-timer-backend
cd server && heroku container:push web --app stage-timer-backend && heroku container:release web --app stage-timer-backend
heroku logs --tail --app stage-timer-backendThe CodeRabbit CLI (cr) reviews local git changes before they are committed. It is already installed and authenticated (coderabbit auth status).
Quick usage:
cr review --agent -t uncommitted # review staged + local edits, structured JSON
cr review --agent --base develop # review everything vs the empty develop baseline
cr doctor # diagnose install/auth/git issuesFor a full-repo review (baseline vs empty develop branch, which the CLI's
three-dot diff cannot do directly because there is no merge base), use a
worktree sandbox:
git worktree add /tmp/cr-wt develop
rsync -a --exclude node_modules --exclude .git --exclude out --exclude .next --exclude .netlify . /tmp/cr-wt/
cd /tmp/cr-wt
setsid nohup cr review --agent --include-untracked > /tmp/cr-review.json 2>/tmp/cr-review.err < /dev/null &Reviews take 7-30+ minutes for a full repo; run them in the background.
Unit + integration tests live in tests/ (server tests in tests/server/,
frontend/component tests in tests/frontend/). Server tests boot the real
Express + WebSocket server on an ephemeral port (see server/server.js, which
exports app/start and only listens when run directly).
npm test # run everything
npm run test:watch # watch mode
npm run test:server # REST + webhook HMAC + WebSocket flows
npm run test:frontend # libs + components (jsdom)Covered so far: webhook HMAC validation (valid/invalid signatures, malformed
totals, missing secret), donation tallies and refunds, session creation,
WebSocket join/auth/role/end flows, formatDuration, session link building and
QR-scan parsing, API/WS URL fallbacks, and the Support (Lemon Squeezy) button
overlay lifecycle.
-
Netlify blocked Next.js due CVE policy
- Upgrade next and eslint-config-next to a patched release, then redeploy.
-
Frontend cannot connect to backend
- Confirm NEXT_PUBLIC_API_URL and NEXT_PUBLIC_WS_URL on Netlify use the hashed Heroku app URL (new apps no longer resolve at
<name>.herokuapp.com). - Confirm PUBLIC_ORIGIN on Heroku matches Netlify production URL.
- Confirm NEXT_PUBLIC_API_URL and NEXT_PUBLIC_WS_URL on Netlify use the hashed Heroku app URL (new apps no longer resolve at
-
error from registry: unsupportedonheroku container:push- Docker containerd image store incompatibility; push with
craneinstead (see notes in Step 1).
- Docker containerd image store incompatibility; push with
-
Webhook returns 401 in Heroku logs
- The X-Signature header must be computed with the exact same secret as LEMON_SQUEEZY_WEBHOOK_SECRET, over the raw request body.
-
Lemon Squeezy webhooks not firing
- Test-mode webhooks only fire for test-mode orders; recreate/update the webhook after disabling test mode.
-
Checkout link 404s ("product not found")
- New Lemon Squeezy stores/products are reviewed before going live. While pending, only the dashboard preview checkout URL works; the
checkout/buy/<numeric-variant-id>URL starts working once the store review passes and you activate the store. Keep?embed=1so the checkout opens as the overlay instead of a new tab. next/fontfails to fetch Google Fonts in some environments: the fonts are self-hosted in src/app/fonts/, so remove that workaround only if you have network access during builds.
- New Lemon Squeezy stores/products are reviewed before going live. While pending, only the dashboard preview checkout URL works; the
- Sessions are in-memory (no database): restarting backend clears active sessions.
- Donation tallies are in-memory too: restarting clears /api/donations.
- For production reliability, enable auto-restart and monitor Heroku logs.