Skip to content

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 5432 from 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_URL
  • TAS_ENCRYPTION_KEY, INTERNAL_API_TOKENone 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:

VariableValue
DATABASE_URLfrom Secrets Manager
TAS_ENCRYPTION_KEYfrom Secrets Manager
INTERNAL_API_TOKENfrom Secrets Manager
RUST_LOGinfo,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:

VariableValue
DATABASE_URLfrom Secrets Manager (better-auth + app CRUD query it directly)
TAS_ENCRYPTION_KEYfrom Secrets Manager (must match api)
INTERNAL_API_TOKENfrom Secrets Manager (must match api)
BETTER_AUTH_SECRETfrom Secrets Manager
BETTER_AUTH_URLhttps://<your-domain>
NEXT_PUBLIC_BETTER_AUTH_URLhttps://<your-domain>
API_INTERNAL_URLhttp://api.<namespace>:8080 (Service Connect DNS)
GOOGLE_CLIENT_IDOAuth 2.0 client. Redirect URI https://<domain>/api/auth/callback/google.
GOOGLE_CLIENT_SECRETfrom 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_EMAILSRequired 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_NAMEoptional 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 web service on port 3000. Redirect :80 → :443.
  • Point your domain (Route 53 or elsewhere) at the ALB.
  • Set BETTER_AUTH_URL / NEXT_PUBLIC_BETTER_AUTH_URL to https://<domain>, and the Google OAuth client’s authorized redirect URI to https://<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. The automation cron (instrumentation.ts) and Composio trigger webhooks (/api/hooks/composio/{workspace}) live in the web container — keep at least one web task running; don’t scale it to zero.
  • Secrets parity. TAS_ENCRYPTION_KEY and INTERNAL_API_TOKEN must be byte-for-byte identical on web and api. Rotating TAS_ENCRYPTION_KEY orphans every existing workspace secret.

Troubleshooting

SymptomLikely 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.