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 Railway
This split is required because the app needs a persistent WebSocket backend.
- Next.js 15
- React 19
- TypeScript
- Tailwind CSS v4
- Express + ws
- pnpm
Prerequisites:
- Node.js 20+
- pnpm
- Free ports: 3000 (frontend) and 8787 (backend)
Install dependencies:
pnpm installRun frontend and backend together:
pnpm run dev:allOpen:
Do not upload .env files to Netlify or Railway. 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.railway.app
- NEXT_PUBLIC_WS_URL=wss://your-backend.railway.app/ws
Backend variables (Railway service variables):
- NODE_ENV=production
- PUBLIC_ORIGIN=https://your-site.netlify.app
- CORS_ALLOW_ALL=0
- SESSION_TTL_MINUTES=120
- SESSION_CODE_ALPHABET=23456789ABCDEFGHJKMNPQRSTUVWXYZ
- PORT=8787 (local fallback; Railway may inject PORT automatically)
- Create a new Railway project.
- Add a service connected to this GitHub repository.
- In service settings, set:
- Build Command: echo "skip frontend build for backend service"
- Start Command: pnpm run server
- Add backend environment variables listed above.
- Generate a public Railway domain for the service.
- Verify health endpoint:
curl https://your-backend.railway.app/api/healthExpected response:
{"ok":true}- Create a Netlify site from this GitHub repository.
- Build settings:
- Build command: pnpm 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.railway.app
- NEXT_PUBLIC_WS_URL=wss://your-backend.railway.app/ws
- Trigger a production deploy.
After Netlify gives you the final production URL:
- Update Railway 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.
Netlify:
pnpm --package=netlify-cli dlx netlify login
pnpm --package=netlify-cli dlx netlify init
pnpm --package=netlify-cli dlx netlify env:set NETLIFY_NEXT_PLUGIN_SKIP true --context production
pnpm --package=netlify-cli dlx netlify env:set NEXT_PUBLIC_API_URL https://your-backend.railway.app --context production
pnpm --package=netlify-cli dlx netlify env:set NEXT_PUBLIC_WS_URL wss://your-backend.railway.app/ws --context production
NEXT_PUBLIC_API_URL=https://your-backend.railway.app NEXT_PUBLIC_WS_URL=wss://your-backend.railway.app/ws pnpm run build
pnpm --package=netlify-cli dlx netlify deploy --prod --no-build --dir outRailway:
pnpm --package=@railway/cli dlx railway login
pnpm --package=@railway/cli dlx railway init -n stagetimer-backend
pnpm --package=@railway/cli dlx railway variable set NODE_ENV=production PUBLIC_ORIGIN=https://your-site.netlify.app CORS_ALLOW_ALL=0 SESSION_TTL_MINUTES=120 SESSION_CODE_ALPHABET=23456789ABCDEFGHJKMNPQRSTUVWXYZ
pnpm --package=@railway/cli dlx railway domain-
Error: Cannot install with frozen lockfile
- Run pnpm install locally and commit updated pnpm-lock.yaml.
-
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.
- Confirm PUBLIC_ORIGIN on Railway matches Netlify production URL.
-
Site returns "Internal Server Error" on Netlify
- If the site was deployed as SSR/runtime and crashes in
___netlify-server-handler, switch to static export deployment (out) and deploy that output. - Ensure
NETLIFY_NEXT_PLUGIN_SKIP=truein production environment.
- If the site was deployed as SSR/runtime and crashes in
- Sessions are in-memory (no database): restarting backend clears active sessions.
- For production reliability, enable auto-restart and monitor Railway logs.