Skip to main content
SST is a TypeScript framework for defining AWS (and other cloud) infrastructure. It’s the cleanest way to deploy Pylon to AWS without writing Terraform — one sst.config.ts provisions Aurora Postgres, an S3 bucket for uploads, an ECS Fargate cluster, a load balancer with WebSocket + SSE + shard ports forwarded, and a CloudFront CDN. The default shape is stateless: Postgres holds app data + sessions, S3 holds files. The container can scale horizontally without losing state. A working reference config ships in deploy/sst/sst.config.ts.

What you get

  • Aurora Serverless v2 Postgres for app data and sessions (auto-scaling 0.5–2 ACU, ~$15/mo minimum)
  • S3 bucket for file uploads (linked to the service for IAM)
  • ECS Fargate running the Pylon container (0.25 vCPU / 512 MB ~ $9/mo, horizontally scalable)
  • Application Load Balancer with WebSocket + SSE + shard port forwarding and sticky sessions
  • AWS Secrets Manager for the admin token + OAuth credentials
  • CloudFront CDN in front of the ALB
  • Route 53 + ACM for custom domains and TLS
Total minimum bill: ~$25/mo for a production deployment.

Prerequisites

You’ll need an AWS account with permissions to create VPCs, ECS services, RDS, ALB, ACM, Route 53, S3, and Secrets Manager. SST’s default IAM role assumes broad permissions; lock down per your org’s standards.

Project layout

The container image must bundle the Pylon binary + Bun — the binary spawns Bun to run your TypeScript functions and SSR. Pylon’s published image (ghcr.io/pylonsync/pylon, which pylon deploy --target docker builds on) already includes both. If you write your own Dockerfile, install Bun (curl -fsSL https://bun.sh/install | bash) or your app’s functions/SSR will fail at runtime.

The default config — Aurora + S3

Credentials are read from env only. Pylon’s S3 backend signs requests with PYLON_S3_ACCESS_KEY / PYLON_S3_SECRET_KEY (plus an optional PYLON_S3_SESSION_TOKEN for temporary/STS credentials). It does not auto-discover the ECS task role from the container credentials endpoint or EC2 IMDS, so link: [uploads] alone isn’t enough — provision a bucket-scoped IAM user’s static key as shown above, or inject the task role’s temporary credentials (access key + secret + session token) into env via your entrypoint.The server validates the S3 env at boot and fails fast if PYLON_FILES_PROVIDER=s3 but a required var is missing — it never silently falls back to local disk (which on a stateless Fargate container would lose uploads on every redeploy).

Local development

Mirror the production stack locally so SQL queries, indexes, and policies behave identically. The cheapest way is Postgres in Docker + local-disk file storage — no MinIO needed for dev unless your code exercises S3-specific behavior. docker-compose.dev.yml:
Boot it once:
Run Pylon against it:
A .env file at the project root keeps this out of your shell history:
pylon dev reads .env automatically; restart the dev server when you change it. To also test against S3 locally, point PYLON_FILES_PROVIDER=s3 at MinIO running in the same docker-compose:
The file_storage plugin treats MinIO as a drop-in S3 — same wire protocol.

Set secrets

Before the first deploy, set the secret values via the SST CLI:
Secrets are stored in AWS Parameter Store, encrypted with a KMS key SST creates per-app.

Deploy

Once the deploy finishes, SST prints the ALB URL. Hit it:

Custom domain

The domain block under loadBalancer provisions an ACM certificate and points Route 53 at the ALB. If your DNS lives elsewhere, swap the dns provider:

Multiple environments

Each stage gets its own Aurora cluster, S3 bucket, and ALB. Use the removal config to make non-prod environments tear down cleanly:

Alternative: single-replica with local state (EFS + SQLite)

If you genuinely want to run Pylon as a single replica with SQLite + local files (e.g. an internal tool, a hobby app, or a hard requirement to avoid Postgres + S3), you can mount EFS instead of using Aurora and S3:
Trade-offs:
  • ✅ ~$10/mo cheaper (no Aurora minimum)
  • ✅ One backing service to think about
  • ❌ Cannot scale horizontally (SQLite is single-writer; mounting EFS into multiple containers corrupts the DB)
  • ❌ EFS latency (~5ms) is meaningfully slower than Aurora (~1ms in-VPC)
  • ❌ Backups are your responsibility (Aurora has them built-in)
Use the default Aurora + S3 shape unless you have a specific reason not to.

CDN in front of the ALB

For static-asset caching and global edge presence:
CloudFront caches GET responses with appropriate Cache-Control headers. WebSocket and SSE traffic should bypass the CDN — point your sync engine’s wsUrl directly at the ALB:

Horizontal scale

The default config already scales horizontally (scaling: { min: 1, max: 4 }). Bump it for read-heavy apps:
Requirements for a correct multi-replica deploy:
  1. Postgres backend ✅ — DATABASE_URL set, no SQLite. The default config satisfies this.
  2. Externalized file storage ⚠️ — the default config points at S3, which isn’t wired yet (see the warning under The default config). Use PYLON_FILES_PROVIDER=stack0 for externalized files, or keep a single replica on the EFS + SQLite shape.
  3. Sticky WebSocket sessions ✅ — stickySessions: true on the load balancer. The default config satisfies this.
  4. Cross-replica live fanout ❌ — WebSocket broadcasts are in-process by default, so a mutation handled by replica A never reaches clients on replica B until their next reconcile(). The default config does not address this; set the cluster bus before scaling past one replica.
For cross-replica fanout, run ElastiCache Redis and point every replica at it with PYLON_CLUSTER_BUS. Pylon refuses to boot if the bus is set but unreachable (loud failure beats silent split-brain). See Horizontal scaling.
For shard-based multiplayer, prefer dedicated single-replica services per region — shards don’t horizontally scale within a region.

Observability

Logs ship to CloudWatch automatically. For richer observability:
  • Metrics: Pylon exposes /metrics in Prometheus format. Scrape with AWS Managed Prometheus or Grafana Cloud.
  • Traces: Add OpenTelemetry exporter env vars and SST will inject the right IAM permissions for X-Ray.

Compared to Pylon Cloud

If you’re not committed to AWS, Pylon Cloud gives you the same managed Postgres + S3 + TLS + WebSocket-aware load balancing with one CLI command (pylon deploy --target cloud) and per-use pricing. SST is the right choice when:
  • You need to live in AWS (compliance, existing infra, RI commitments)
  • You want to compose Pylon with other AWS services (Bedrock, SageMaker, IoT Core, Kinesis)
  • You want full IaC control over networking, IAM, secrets
Both are valid. Pylon’s wire protocol is identical regardless of how you host it.

Troubleshooting

WebSocket connections fail with 504 — ALB idle timeout defaults to 60s. The default config bumps it to 1 hour via idleTimeout: "3600 seconds". Cold start takes 10+ minutes the first time — Aurora Serverless v2’s first cold start provisions storage. Subsequent deploys are <2 min. Uploads don’t land in S3 — expected: the runtime’s storage selector doesn’t wire the S3 backend yet (see the warning under The default config), so PYLON_FILES_PROVIDER=s3 writes to local container disk. Use PYLON_FILES_PROVIDER=stack0 for externalized storage, or the single-replica EFS + SQLite shape. (The link: [uploads] IAM grant is still correct for when S3 lands.) Local dev errors connection refused — Postgres isn’t running. docker compose -f docker-compose.dev.yml ps should show the db service Up. If not: docker compose up -d db. Local dev sessions disappear on restart — sessions are stored in Postgres now (the same DATABASE_URL), so they persist as long as the Postgres volume does. If you docker compose down -v (note -v), the volume is destroyed and sessions clear. Secrets aren’t visible in the container — make sure you ran sst secret set in the same stage you’re deploying to. Secrets are per-stage.

Cost optimization

A bare-bones non-prod stack runs ~30/mo;productionwithNAT+multiAZruns 30/mo; production with NAT + multi-AZ runs ~80–150/mo depending on traffic. For low-traffic apps, Pylon Cloud is often cheaper than the AWS minimums.

Reference config

The full working config is at deploy/sst/sst.config.ts. Clone it as a starting point: