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 them in
manifest.auth.trustedOrigins, or 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 must forward all relevant ports. For deployments with a single public port (Fly.io, Cloud Run), path-route the WebSocket, SSE, and shard upgrades through that one port at the proxy (see the Caddy and nginx examples below), then point the client’s sync
wsUrl at the public origin. There is no server-side WS-URL environment variable; the client’s transport config tells it where to connect.
Shape 1: single VPS (SSH + systemd)
This is the simplest and cheapest shape: the pylon binary plus Bun, one systemd unit, and one reverse proxy.pylon deploy --target systemd generates this unit, plus step-by-step install commands, for your app. The shape looks like this:
: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 with cron. Test the restore quarterly, using 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 set 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. It uses the same components (Aurora, Fargate, ALB, EFS, CloudFront) and one CLI for deploys, secrets, and per-PR preview environments.
- secrets
- custom domains
- multi-port load balancer config
- EFS vs S3 storage
- horizontal scaling
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. Idle apps cost $0, because Workers scales to zero. Cost rises with request volume. See Workers costs.
Workers does not yet support realtime shards (tick-based sims). They need persistent state that Workers alone cannot 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, and permissive CORS. Do not use this mode in 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 yet install a signal handler, so SIGTERM does not run an in-process drain. The drain path exists in the runtime, but it is not wired to a signal yet. Two things make an abrupt stop safe in practice:
- SQLite (WAL mode) is crash-consistent. An abrupt stop does not corrupt the database. The next start recovers the WAL, so there is no unflushed data to lose.
- Drain at the load balancer, not in the process. For a rolling deploy 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.
PYLON_DRAIN_SECS (default 10s) bounds the drain loop for paths that do invoke the runtime’s shutdown, such as a programmatic shutdown. It is not a SIGTERM grace period.
Native clients (iOS, macOS, Android)
Native clients hit the same HTTP and WebSocket endpoints as browsers. Keep these points in mind:- CORS does not apply. Native HTTP clients skip CORS entirely.
PYLON_CORS_ORIGINdoes not affect them. - TLS is required on iOS. App Transport Security rejects
ws://andhttp://to non-localhost hosts. Always usewss://andhttps://in production. Self-signed certificates work in dev with an ATS exception inInfo.plist. - Background sessions. For large file uploads or downloads that should continue when the app is backgrounded, configure
URLSessionConfiguration.background(...)in your transport. See Swift SDK.
Scale-out
Pylon supports multiple processes behind a load balancer. Use Postgres for shared entities, auth state, jobs, workflows, and sync state. ConfigurePYLON_CLUSTER_BUS for live change, presence, and CRDT fanout between self-hosted processes. Pylon Cloud configures its managed relay for you.
Jobs and workflow steps use at-least-once execution. Make their handlers idempotent. SQLite remains a single-machine backend.
See Horizontal scaling for the full setup and failure model.
For multi-region on Pylon Cloud, set the workspace region during signup. Self-hosted multi-region is a manual setup: one Pylon process per region, with app-level routing.
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 commands generate a Dockerfile that copies your app onto the runtime image and serves it with pylon start.