Deploy on AWS
This guide deploys TAS on ECS Fargate with RDS for Postgres:
managed containers and managed, automatically-backed-up Postgres. Both
services run the published GHCR images (ghcr.io/tembo/tas-api,
ghcr.io/tembo/tas-web — public, so no ECR mirroring required).
The one AWS-specific wrinkle versus Vercel/Railway is TLS: AWS doesn’t hand you HTTPS for free, and better-auth needs an HTTPS origin in production (secure cookies + the Google OAuth redirect). Step 5 terminates it at an ALB.
Architecture target
Browser ──HTTPS──► ALB (ACM cert) ──► web (Fargate) ──► api (Fargate) ──► RDS │ │ │ └─ both query RDS ─┴──────────────┘api is never public — web reaches it over the VPC via ECS Service
Connect. Only web sits behind the ALB.
1. RDS Postgres
- Postgres 16 or newer (18 recommended), in private subnets, with automated backups enabled (the whole reason we’re here).
gen_random_uuid()is built in (PG13+); no extension setup needed.- Security group: inbound
5432from the ECS task security group only. - Record the connection string as
DATABASE_URL.
The Rust api applies migrations on boot via sqlx::migrate!(), so
there’s no manual migration step.
2. Secrets Manager
Store these and reference them from the task definitions (so they never sit in plaintext task JSON):
DATABASE_URLTAS_ENCRYPTION_KEY,INTERNAL_API_TOKEN— one value each, shared by both task defs.openssl rand -base64 32. Mismatch = undecryptable secrets / 401s between the tiers.BETTER_AUTH_SECRET(web only).openssl rand -base64 32.GOOGLE_CLIENT_SECRET(web only).
3. Task definitions (pull the public images)
api task — image ghcr.io/tembo/tas-api:<version>, container port
8080, no public ingress. Env:
| Variable | Value |
|---|---|
DATABASE_URL | from Secrets Manager |
TAS_ENCRYPTION_KEY | from Secrets Manager |
INTERNAL_API_TOKEN | from Secrets Manager |
RUST_LOG | info,tas_api=debug |
(AWS VPC networking is IPv4, so the default bind is reachable — no
API_BIND_ADDR override needed.)
web task — image ghcr.io/tembo/tas-web:<version>, container port
3000. Env:
| Variable | Value |
|---|---|
DATABASE_URL | from Secrets Manager (better-auth + app CRUD query it directly) |
TAS_ENCRYPTION_KEY | from Secrets Manager (must match api) |
INTERNAL_API_TOKEN | from Secrets Manager (must match api) |
BETTER_AUTH_SECRET | from Secrets Manager |
BETTER_AUTH_URL | https://<your-domain> |
NEXT_PUBLIC_BETTER_AUTH_URL | https://<your-domain> |
API_INTERNAL_URL | http://api.<namespace>:8080 (Service Connect DNS) |
GOOGLE_CLIENT_ID | OAuth 2.0 client. Redirect URI https://<domain>/api/auth/callback/google. |
GOOGLE_CLIENT_SECRET | from Secrets Manager |
MICROSOFT_* / OIDC_* | Optional alternative providers (Entra ID / any OIDC IdP). Redirect URIs https://<domain>/api/auth/oauth2/callback/{microsoft,oidc}. See .env.example. |
INSTANCE_ADMIN_EMAILS | Required to bootstrap — comma-separated instance-admin emails. The instance is invite-only; only these admins can sign in to a fresh deployment and create workspaces / invite others. |
TAS_INSTANCE_NAME | optional brand label |
4. web → api private networking
Enable ECS Service Connect (or Cloud Map) on the cluster so web
resolves api by name. Set API_INTERNAL_URL to the api service’s
Service Connect DNS (http://api.<namespace>:8080). The api service
needs no load balancer — it’s internal only.
5. Public TLS (ALB + ACM)
- Request an ACM certificate for your domain (same region as the ALB).
- Application Load Balancer: an HTTPS:443 listener with the ACM cert →
target group → the
webservice on port3000. Redirect:80 → :443. - Point your domain (Route 53 or elsewhere) at the ALB.
- Set
BETTER_AUTH_URL/NEXT_PUBLIC_BETTER_AUTH_URLtohttps://<domain>, and the Google OAuth client’s authorized redirect URI tohttps://<domain>/api/auth/callback/google.
6. Deploy + verify
Bring up the api service first (it migrates the RDS schema on boot),
then web. Then:
- Open
https://<domain>and sign in with Google — the first user becomes workspace admin on first workspace creation. - Connect a GitHub repo (Settings → Repository).
- Trigger one manual run and confirm it lands in
/runs.
Operational notes
- Upgrades. Bump the image tag in both task defs and redeploy; the
api migrates on boot. Pin versions, not
latest, for reproducible rollouts. - Scheduler + webhooks run on
web. Theautomationcron (instrumentation.ts) and Composio trigger webhooks (/api/hooks/composio/{workspace}) live in the web container — keep at least onewebtask running; don’t scale it to zero. - Secrets parity.
TAS_ENCRYPTION_KEYandINTERNAL_API_TOKENmust be byte-for-byte identical on web and api. RotatingTAS_ENCRYPTION_KEYorphans every existing workspace secret.
Troubleshooting
| Symptom | Likely cause |
|---|---|
exec format error on container start. | Ran the amd64 image on arm64/Graviton Fargate. Use the X86_64 platform. |
| Sign-in succeeds, then a 401 loop. | BETTER_AUTH_URL isn’t the real HTTPS origin, or TLS isn’t terminating in front of web (cookies need https). |
| Runs queue but never start. | web can’t reach api: wrong API_INTERNAL_URL, or Service Connect not enabled. /internal/* 401s mean INTERNAL_API_TOKEN mismatch. |
failed to decrypt secret in api logs. | TAS_ENCRYPTION_KEY differs between web and api. |
| api can’t reach Postgres. | RDS security group doesn’t allow the task SG on 5432, or DATABASE_URL host is wrong. |