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
Prerequisites
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:
.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:
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:Deploy
Custom domain
Thedomain 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
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:- ✅ 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)
CDN in front of the ALB
For static-asset caching and global edge presence: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:
- Postgres backend ✅:
DATABASE_URLis set, with no SQLite. The default config satisfies this requirement. - 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=stack0for externalized files, or keep a single replica on the EFS and SQLite shape. - Sticky WebSocket sessions ✅:
stickySessions: trueon the load balancer. The default config satisfies this requirement. - 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.
PYLON_CLUSTER_BUS. Pylon refuses to boot if the bus is set but unreachable (loud failure beats silent split-brain). See Horizontal scaling.
Observability
- Metrics: Pylon exposes
/metricsin 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
Troubleshooting
WebSocket connections fail with 504. The ALB idle timeout defaults to 60s. The default config bumps it to 1 hour withidleTimeout: "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 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 atdeploy/sst/sst.config.ts. Clone it as a starting point: