Required environment
PYLON_CORS_ORIGIN=*in non-dev mode is refused- In non-dev mode, the CORS gate refuses to start with no trusted origins — declare in
manifest.auth.trustedOriginsor setPYLON_CORS_ORIGIN PYLON_DEV_MODE=truewithPYLON_ADMIN_TOKENunset is refused/api/__test__/resetis disabled unless dev + in-memory + loopback
Ports
Pylon uses up to four adjacent ports, depending on which transports you enable:
Your reverse proxy needs to forward all relevant ports. For deployments with a single public port (Fly.io, Cloud Run), path-route the WebSocket/SSE/shard upgrades through that one port at the proxy (see the Caddy/nginx examples below) and point the client’s sync
wsUrl at the public origin — there’s no server-side WS-URL env var; the client is told where to connect via its transport config.
Shape 1: single VPS (SSH + systemd)
Simplest and cheapest. The pylon binary plus Bun, one systemd unit, one reverse proxy.pylon deploy --target systemd generates this unit (plus step-by-step install commands) for your app; the shape looks like:
:443 → :4321 plus WebSocket upgrades for /ws → :4322, /events (SSE) → :4323, and shard WS → :4324. pylon deploy --target systemd emits a matching nginx or Caddy reverse-proxy config alongside the unit file (generated by crates/runtime/src/tls.rs).
pylon backup /var/backups/pylon/$(date +%F) nightly. Test restore quarterly per the test at crates/runtime/tests/backup_restore.rs.
Caddy example
Shape 2: AWS ECS + Aurora (Terraform)
deploy/terraform/modules/pylon/ provisions:
- ECS Fargate service (0.25 vCPU, 512 MB) ~$9/mo
- Aurora Serverless v2 (0.5–2 ACU) ~$15/mo minimum
- ALB with TLS + WebSocket routing
- CloudFront CDN + Route53 DNS
--features postgres-live and DATABASE_URL=postgres://....
Shape 3: AWS via SST (TypeScript IaC)
For TypeScript-first infrastructure, SST v3 provisions the same AWS shape from a singlesst.config.ts. Same components (Aurora, Fargate, ALB, EFS, CloudFront), one CLI for deploys + secrets + per-PR preview environments.
Shape 4: Cloudflare Workers (edge, experimental)
crates/workers/ builds a WASM bundle (worker-build --release) that runs on Workers with a D1 binding. See crates/workers/README.md for current limitations. Scale-to-zero: idle apps cost $0. Cost rises with actual request volume. See Workers costs.
Realtime shards (tick-based sims) are not yet supported on Workers — they need persistent state that Workers-only can’t hold efficiently. Use shape 1 or 2 for game shards.
Shape 5: Pylon Cloud
Shape 6: local dev
PYLON_DEV_MODE=true defaults. Studio at /studio, hot-reload, permissive CORS. Not for production.
Health checks
GET /healthreturns 200 with{"status":"ok","uptime_seconds":N}GET /metricsreturns Prometheus text whenAccept: text/plainGET /readyzchecks DB connectivity
Shutdown and rolling deploys
SIGTERM terminates the process. The runtime does not currently install a
signal handler, so SIGTERM does not run an in-process drain — the drain path
exists in the runtime but isn’t yet wired to a signal. Two things make an
abrupt stop safe in practice:
- SQLite (WAL mode) is crash-consistent. An abrupt stop won’t corrupt the database; the next start recovers the WAL. There’s no “flush on exit” to lose.
- Drain at the load balancer, not in the process. For rolling deploys with zero dropped requests: stop routing new traffic to the old instance, wait out your load balancer’s connection-drain / deregistration delay so in-flight requests finish, then stop the process.
SIGTERM it.
(PYLON_DRAIN_SECS, default 10s, bounds the drain loop for the paths that do
invoke the runtime’s shutdown — e.g. programmatic shutdown; it is not a SIGTERM
grace period.)
Native clients (iOS, macOS, Android)
Native clients hit the same HTTP + WebSocket endpoints as browsers, with two notes:- CORS doesn’t apply — native HTTP clients skip CORS entirely. Don’t worry about
PYLON_CORS_ORIGINfor them. - TLS is mandatory on iOS — App Transport Security rejects
ws://andhttp://to non-localhost hosts. Always usewss://andhttps://in production. Self-signed certs work in dev with an ATS exception inInfo.plist. - Background sessions — for large file uploads/downloads that should continue when the app is backgrounded, configure
URLSessionConfiguration.background(...)in your transport. See Swift SDK.
Scale-out
Single-process by design. For higher throughput:- Reads: cache + rely on the 4-connection read pool (already in)
- Writes: move to Postgres (
postgres-livefeature) - WS fanout: workers + Durable Objects; shape 3 amortizes edge
- Shards: run one process per game region; load-balance by match id
What about Docker?
The runtime image bundles the pylon binary plus Bun (which runs your TypeScript functions and SSR). It expects your app source mounted (or baked) at/app and keeps data on the /data volume:
pylon deploy --target docker or pylon deploy --target compose in your project — both generate a Dockerfile that copies your app onto the runtime image and serves it with pylon start.