Skip to main content
SST is a TypeScript framework for defining AWS (and other cloud) infrastructure. It is 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, and a CloudFront CDN. The load balancer forwards WebSocket, SSE, and shard ports. The default shape is stateless. Postgres holds app data and sessions, and 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, and shard port forwarding, and sticky sessions
  • AWS Secrets Manager for the admin token and OAuth credentials
  • CloudFront CDN in front of the ALB
  • Route 53 and ACM for custom domains and TLS
Total minimum bill: ~$25/mo for a production deployment.

Prerequisites

You will 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 it down to match your organization’s standards.

Project layout

The container image must bundle the Pylon binary and 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 and SSR will fail at runtime.

The default config: Aurora and S3

Credentials are read from environment variables only. Pylon’s S3 backend signs requests with PYLON_S3_ACCESS_KEY and PYLON_S3_SECRET_KEY (plus an optional PYLON_S3_SESSION_TOKEN for temporary or STS credentials). It does not auto-discover the ECS task role from the container credentials endpoint or EC2 IMDS, so link: [uploads] alone is not enough. Provision a bucket-scoped IAM user’s static key as shown above, or inject the task role’s temporary credentials (access key, secret, and session token) into the environment through your entrypoint.The server validates the S3 environment at boot and fails fast if PYLON_FILES_PROVIDER=s3 but a required variable 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 the same way. The cheapest setup is Postgres in Docker plus local-disk file storage. You do not need MinIO 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 replacement. It uses the same wire protocol.

Set secrets

Before the first deploy, set the secret values with 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. Test it:

Custom domain

The domain block under loadBalancer provisions an ACM certificate and points Route 53 at the ALB. If you use a different DNS provider, swap the dns provider:

Multiple environments

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

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

If you want to run Pylon as a single replica with SQLite and local files (for example, an internal tool, a hobby app, or a hard requirement to avoid Postgres and S3), mount EFS instead of using Aurora and S3:
Trade-offs:
  • ✅ About $10/mo cheaper (no Aurora minimum)
  • ✅ One backing service to think about
  • ❌ Cannot scale horizontally: SQLite is single-writer, and mounting EFS into multiple containers corrupts the database
  • ❌ EFS latency (about 5ms) is noticeably slower than Aurora (about 1ms in-VPC)
  • ❌ Backups are your responsibility (Aurora has them built in)
Use the default Aurora and 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 }). Increase it for read-heavy apps:
Requirements for a correct multi-replica deploy:
  1. Postgres backend ✅: DATABASE_URL is set, with no SQLite. The default config satisfies this requirement.
  2. Externalized file storage ⚠️: the default config points at S3, which is not 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 and SQLite shape.
  3. Sticky WebSocket sessions ✅: stickySessions: true on the load balancer. The default config satisfies this requirement.
  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.
Jobs and workflows need no Redis. They use the shared Aurora database and can fail over between replicas. Their handlers must be idempotent because execution is at least once. 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 do not scale horizontally 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 environment variables, and SST injects the right IAM permissions for X-Ray.

Compared to Pylon Cloud

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

Troubleshooting

WebSocket connections fail with 504. The ALB idle timeout defaults to 60s. The default config bumps it to 1 hour with idleTimeout: "3600 seconds". Cold start takes 10+ minutes the first time. Aurora Serverless v2’s first cold start provisions storage. Subsequent deploys take under 2 minutes. Uploads do not land in S3. This is expected: the runtime’s storage selector does not 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 use the single-replica EFS and SQLite shape instead. (The link: [uploads] IAM grant is still correct for when S3 support lands.) Local dev errors with connection refused. Postgres is not running. docker compose -f docker-compose.dev.yml ps should show the db service as Up. If it does not, run 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 run docker compose down -v (note the -v flag), the volume is destroyed and sessions clear. Secrets are not visible in the container. Make sure you ran sst secret set in the same stage you are deploying to. Secrets are set per stage.

Cost optimization

A bare-bones non-production stack runs about 30/mo.ProductionwithNATandmultiAZrunsabout30/mo. Production with NAT and multi-AZ runs about 80 to $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: