Skip to content

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

Terminal window
# Clone the repository
git clone https://github.com/FiLORUX/punch.git
cd punch
# Install dependencies
npm install
# Authenticate with Cloudflare
wrangler login
# Set the signing secret for token authentication
wrangler secret put PUNCH_SECRET
# Enter a random 64+ character string when prompted
# Deploy
wrangler deploy

That’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

VariableTypeRequiredDescription
PUNCH_SECRETSecretYesHMAC-SHA256 signing key for session tokens. Use a 64+ character random string.
TURNSTILE_SECRETSecretNoCloudflare Turnstile secret. When unset, Turnstile verification is bypassed entirely (suitable for local dev or trusted deployments).
TURNSTILE_SITE_KEYVarNoPublic Turnstile site key, paired with the secret above. Visible in client-side HTML; safe to commit.
PUNCH_DEFAULT_TTLVarNoDefault session TTL in seconds (default: 1800).
PUNCH_MAX_STREAMSVarNoMaximum streams per session (default: 16).
PUNCH_RATE_LIMITVarNoSession-creation rate limit per IP per minute (default: 10).

Set secrets:

Terminal window
wrangler secret put PUNCH_SECRET
wrangler secret put TURNSTILE_SECRET # optional

Set variables in wrangler.toml under [vars].

Custom domain setup

Option 1: Custom domain via Cloudflare Dashboard

  1. Go to Workers & Pages → punch → Settings → Domains & Routes
  2. Add custom domain: punch.yourdomain.se
  3. Cloudflare creates the DNS record automatically

Option 2: Route in wrangler.toml

routes = [
{ pattern = "punch.yourdomain.se/*", zone_name = "yourdomain.se" }
]

Then deploy:

Terminal window
wrangler deploy

DNS

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:

ResourceFree limitPunch usage per session
Worker requests100,000/day~5-10 (session create, QR, connect)
DO requests100,000/day (shared)~30-50 per hour (WebSocket messages)
DO storage (SQLite)5 GB~1-10 KB per session
Worker CPU10ms per invocation<1ms typical (routing only)

Practical capacity on free tier:

ScenarioSessions/dayConcurrent
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:

ResourcePaid included
Worker requests10,000,000/month
DO requests1,000,000/month
DO storage25B reads, 50M writes/month
Worker CPU30s per invocation

At $5/month, Punch handles thousands of concurrent sessions.

Development

Local development

Terminal window
# Start local dev server with Durable Object support
wrangler dev
# The dev server runs at http://localhost:8787
# Durable Objects work locally via wrangler's built-in simulator

Note: WebSocket connections work in local dev mode. The Hibernation API behaves identically to production.

Testing

Terminal window
# Create a test session
curl -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.md

Monitoring

Cloudflare Dashboard

Workers & Pages → punch → Analytics:

  • Request count and latency
  • Error rate
  • CPU time usage
  • Durable Object request count

Logs

Terminal window
# Tail live logs
wrangler tail
# Filter by status
wrangler tail --status error
# Filter by search term
wrangler tail --search "session"

Health check

Terminal window
# Verify the Worker is responding
curl https://punch.yourdomain.se/api/health
# Expected response
{ "status": "ok", "version": "1.0.0" }

Updating

Terminal window
# Pull latest changes
git pull
# Deploy update
wrangler deploy

Important: 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 git
  • PUNCH_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 instance
name = "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

SymptomCauseFix
401 on all requestsMissing or wrong PUNCH_SECRETwrangler secret put PUNCH_SECRET
WebSocket connects but no peer infoSecond peer hasn’t registered yetWait — Punch notifies when both peers are present
SRT connects but no videoWrong passphrase or latencyCheck auto-generated connection string
”Session not found”Session TTL expiredCreate a new session
High latency on signallingDO in distant regionFirst request to a session determines DO region