Deployment
How to deploy Punch on Cloudflare Workers.
Prerequisites
- A Cloudflare account (free tier is sufficient)
- Node.js 18+ and npm
- Wrangler CLI (
npm install -g wrangler) - A domain managed by Cloudflare (for
punch.yourdomain.se)
Quick deploy
# Clone the repositorygit clone https://github.com/FiLORUX/punch.gitcd punch
# Install dependenciesnpm install
# Authenticate with Cloudflarewrangler login
# Set the signing secret for token authenticationwrangler secret put PUNCH_SECRET# Enter a random 64+ character string when prompted
# Deploywrangler deployThat’s it. Punch is live.
Configuration
wrangler.toml
The reference deployment uses an SQLite-backed Durable Object and the
nodejs_compat_v2 flag. The shape below mirrors the upstream
wrangler.toml exactly — copy-paste it and change the route to match
your zone.
name = "punch"main = "src/index.ts"compatibility_date = "2026-02-24"compatibility_flags = ["nodejs_compat_v2"]
# Custom domain (optional — falls back to workers.dev)routes = [ { pattern = "punch.example.se/*", zone_name = "example.se" }]
# Durable Object binding — inline form, one block per binding.[durable_objects]bindings = [ { name = "SESSION_ROOM", class_name = "SessionRoom" }]
# SQLite-backed Durable Object migration. Use new_sqlite_classes (not# new_classes) so the DO storage backend is SQLite — required for# session state, alarm scheduling, and hibernatable WebSockets in# the current Workers runtime.[[migrations]]tag = "v1"new_sqlite_classes = ["SessionRoom"]
# Stub the few Node fs imports pulled in transitively by libraries# that nodejs_compat_v2 doesn't itself shim. The stub returns empty# stand-ins; nothing in the runtime path actually touches the disk.[alias]fs = "./src/stubs/fs.ts""node:fs" = "./src/stubs/fs.ts"
[vars]PUNCH_DEFAULT_TTL = "1800"PUNCH_MAX_STREAMS = "16"PUNCH_RATE_LIMIT = "10"# Cloudflare Turnstile site key. Public — safe to commit. Pair with# the TURNSTILE_SECRET set via `wrangler secret put TURNSTILE_SECRET`.# Leave the secret unset to disable Turnstile (e.g. local dev).TURNSTILE_SITE_KEY = "0x0000000000000000000000"Environment variables
| Variable | Type | Required | Description |
|---|---|---|---|
PUNCH_SECRET | Secret | Yes | HMAC-SHA256 signing key for session tokens. Use a 64+ character random string. |
TURNSTILE_SECRET | Secret | No | Cloudflare Turnstile secret. When unset, Turnstile verification is bypassed entirely (suitable for local dev or trusted deployments). |
TURNSTILE_SITE_KEY | Var | No | Public Turnstile site key, paired with the secret above. Visible in client-side HTML; safe to commit. |
PUNCH_DEFAULT_TTL | Var | No | Default session TTL in seconds (default: 1800). |
PUNCH_MAX_STREAMS | Var | No | Maximum streams per session (default: 16). |
PUNCH_RATE_LIMIT | Var | No | Session-creation rate limit per IP per minute (default: 10). |
Set secrets:
wrangler secret put PUNCH_SECRETwrangler secret put TURNSTILE_SECRET # optionalSet variables in wrangler.toml under [vars].
Custom domain setup
Option 1: Custom domain via Cloudflare Dashboard
- Go to Workers & Pages → punch → Settings → Domains & Routes
- Add custom domain:
punch.yourdomain.se - Cloudflare creates the DNS record automatically
Option 2: Route in wrangler.toml
routes = [ { pattern = "punch.yourdomain.se/*", zone_name = "yourdomain.se" }]Then deploy:
wrangler deployDNS
If using a subdomain on a Cloudflare-managed zone, the DNS record is created automatically by the custom domain binding. No manual DNS configuration needed.
For IDN domains (like thåst.se), use the punycode form in zone references:
routes = [ { pattern = "punch.xn--thst-roa.se/*", zone_name = "xn--thst-roa.se" }]Free tier limits
Punch is designed to run within Cloudflare’s free tier:
| Resource | Free limit | Punch usage per session |
|---|---|---|
| Worker requests | 100,000/day | ~5-10 (session create, QR, connect) |
| DO requests | 100,000/day (shared) | ~30-50 per hour (WebSocket messages) |
| DO storage (SQLite) | 5 GB | ~1-10 KB per session |
| Worker CPU | 10ms per invocation | <1ms typical (routing only) |
Practical capacity on free tier:
| Scenario | Sessions/day | Concurrent |
|---|---|---|
| Light use (dev/testing) | ~1,000 | ~20 |
| Moderate (small broadcaster) | ~500 | ~50 |
| Heavy (multi-show production) | ~200 | ~50 (longer sessions) |
The binding constraint is the 100,000 request/day budget shared between Workers and Durable Objects.
When to upgrade
The Workers Paid plan ($5/month) provides:
| Resource | Paid included |
|---|---|
| Worker requests | 10,000,000/month |
| DO requests | 1,000,000/month |
| DO storage | 25B reads, 50M writes/month |
| Worker CPU | 30s per invocation |
At $5/month, Punch handles thousands of concurrent sessions.
Development
Local development
# Start local dev server with Durable Object supportwrangler dev
# The dev server runs at http://localhost:8787# Durable Objects work locally via wrangler's built-in simulatorNote: WebSocket connections work in local dev mode. The Hibernation API behaves identically to production.
Testing
# Create a test sessioncurl -X POST http://localhost:8787/api/session \ -H 'Content-Type: application/json' \ -d '{"name": "test-session"}'
# Connect via WebSocket (use wscat or similar)wscat -c "ws://localhost:8787/api/ws/test-session?token=TOKEN_FROM_ABOVE"Project structure
punch/├── src/│ ├── index.ts Edge Worker (router, auth)│ ├── session-room.ts Durable Object (session state, WebSocket hub)│ ├── auth.ts Token generation and validation│ ├── protocol.ts Message type definitions│ └── ui/ Web UI assets│ ├── session.html Session dashboard page│ └── qr.ts QR code generation├── docs/ Documentation├── wrangler.toml Cloudflare configuration├── package.json├── tsconfig.json└── README.mdMonitoring
Cloudflare Dashboard
Workers & Pages → punch → Analytics:
- Request count and latency
- Error rate
- CPU time usage
- Durable Object request count
Logs
# Tail live logswrangler tail
# Filter by statuswrangler tail --status error
# Filter by search termwrangler tail --search "session"Health check
# Verify the Worker is respondingcurl https://punch.yourdomain.se/api/health
# Expected response{ "status": "ok", "version": "1.0.0" }Updating
# Pull latest changesgit pull
# Deploy updatewrangler deployImportant: Deploying a new version disconnects all active WebSocket connections. Clients should implement automatic reconnection. Plan deployments during low-activity periods for production use.
Backup and recovery
Punch is stateless by design — sessions are ephemeral and expire via TTL. There is nothing to back up.
The only persistent configuration is:
wrangler.toml— checked into gitPUNCH_SECRET— stored as a Cloudflare secret
If the secret is lost, generate a new one. All existing tokens will be invalidated, but new sessions work immediately.
Multiple instances
For organisations that want separate Punch instances (e.g., per-department or per-production):
# wrangler.toml for a second instancename = "punch-sports"routes = [ { pattern = "punch-sports.yourdomain.se/*", zone_name = "yourdomain.se" }]Each instance has its own Durable Objects, secrets, and session namespace. They are fully independent.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 401 on all requests | Missing or wrong PUNCH_SECRET | wrangler secret put PUNCH_SECRET |
| WebSocket connects but no peer info | Second peer hasn’t registered yet | Wait — Punch notifies when both peers are present |
| SRT connects but no video | Wrong passphrase or latency | Check auto-generated connection string |
| ”Session not found” | Session TTL expired | Create a new session |
| High latency on signalling | DO in distant region | First request to a session determines DO region |