Migrate infrastructure to AWS (SST + Fargate + RDS + Clerk + S3 + SES)
From the PR description
Summary
Full migration from the upstream Cloudflare/Supabase stack to an AWS-native stack. Single branch, 9 logically distinct commits.
| Layer | Before | After |
|---|---|---|
| Frontend hosting | Cloudflare Workers (@opennextjs/cloudflare) |
CloudFront + Lambda via @opennextjs/aws, driven by sst.aws.Nextjs |
| Backend hosting | nixpacks-built Node app | ECS Fargate behind an ALB, containerized image with LibreOffice (backend/Dockerfile) |
| Database | Supabase Postgres + RLS | Aurora Serverless v2 + RDS Proxy, accessed via Drizzle ORM. All RLS dropped - access checks live in route handlers. |
| Auth | Supabase Auth (@supabase/supabase-js) |
Clerk (@clerk/nextjs + @clerk/backend). user_id columns are now text (Clerk IDs) instead of uuid. |
| Object storage | Cloudflare R2 | S3 (native AWS SDK, IAM-role credentials on Fargate) |
| Resend | SES via @aws-sdk/client-sesv2 (helper added; no current call sites in upstream) |
|
| Secrets | .env files |
SST Secrets → AWS Secrets Manager, mounted on the Fargate task |
| IaC | None (nixpacks) | SST v3 - single sst.config.ts covers VPC, RDS, S3, Fargate, Nextjs, and all secrets |
Stage-by-stage commits
e3ee400- infra: SST scaffold + Fargate Dockerfilecea1546- db: Drizzle schema (16 tables) + initial migration; swapsupabase-jsdepfcd1bea- auth: Clerk JWT verification + Drizzle client; first-request profile bootstrap replaces thehandle_new_usertrigger55d189d- routes (E1): simpler routes +access/userSettings/userApiKeyslibs to Drizzle8d56028- routes (E2): heavy routes (projects/documents/tabular/chat) +chatTools(3284 lines) to Drizzle; backendtsc --noEmitreaches 0 errorsc599ba5- storage+email: R2 → S3 inbackend/src/lib/storage.ts; Resend → SES helper addede20c5e9- frontend auth:<ClerkProvider>,clerkMiddleware, catch-all/login+/signup, all 8supabase.auth.getSession()call sites + 12useAuthconsumers migrated; 6 dead Supabase files deleted01b10ca- frontend deploy:@opennextjs/cloudflare→@opennextjs/aws;wranglerremoved; Cloudflare-only scripts dropped2c0806a- docs: README + CLAUDE.md updated for the new stack
Notable design decisions
- Drizzle over a Supabase-compatible façade. Considered building a
@supabase/supabase-js-shaped adapter on top ofpgto minimize route diffs, but every callsite gets rewritten explicitly. Cleaner, type-safe, no leaky abstractions. - First-request profile bootstrap in the auth middleware with an in-process
Set<string>cache replaces the dropped Supabasehandle_new_usertrigger. - Clerk's
users.getUserList({ emailAddress: [...] })replaces Supabase'sauth.admin.listUsersfor member lookups inprojects.tsandtabular.ts- more efficient than the old "list 1000 users and filter" pattern. backend/schema.sqlkept on disk as a no-op file to ease upstream merges (the real schema source is nowbackend/src/db/schema.ts).nixpacks.tomlandfrontend/open-next.config.tsleft in place (rewritten in H, but the file is still there) so upstream PRs to those paths still 3-way-merge.
Verification
npx tsc --noEmit -p backend: 0 errorsnpx tsc --noEmit -p frontend: 0 errorsnpm run lint --prefix frontend: 104 problems - all pre-existing, none introduced by this PR- Zero remaining
@supabase/*imports anywhere inbackend/srcorfrontend/src
Deployment checklist (post-merge)
-
npm installat repo root (SST CLI) -
npx sst secret setfor every secret declared insst.config.ts: Clerk publishable/secret/JWT keys, Anthropic/Gemini/OpenAI keys,UserApiKeysEncryptionSecret,DownloadSigningSecret,SesFromAddress - Verify the SES sending identity in the target region
-
npx sst deploy --stage production - Connect to RDS via the SST-provisioned bastion/SSM session and run
backend/drizzle/0000_init.sql(ornpm run db:migrate --prefix backendwithDATABASE_URLpointed at RDS) - Configure the Clerk application to issue tokens with
emailclaim if you want to skip the per-user Clerk API lookup on first request - Update DNS to point
app.<domain>at the CloudFront distribution SST creates
Upstream-fork strategy
infra/UPSTREAM.md documents how this fork should pull from upstream/main. Expect conflicts on every upstream PR that touches backend/src/routes/*, backend/src/middleware/auth.ts, backend/src/lib/storage.ts, or the frontend auth files. Non-conflicting paths: infra/, backend/Dockerfile, sst.config.ts, backend/src/db/.
Test plan
-
sst deployto a staging stage and walk through the golden path: sign up via Clerk, create a project, upload a DOCX, run a chat, run a workflow, run a tabular review, download a generated DOCX - Verify LibreOffice is on the Fargate task PATH (
/usr/bin/soffice) by triggering a DOC→PDF conversion - Verify RDS Proxy connection pooling under burst load
- Verify Clerk JWKS verification works end-to-end (or set
CLERK_JWT_KEYfor offline verification) - Verify S3 presigned upload + download URLs work from the browser
- Verify SES sends from the verified domain
🤖 Generated with Claude Code
Our analysis
Full AWS-native rewrite of the Mike stack — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-96.md from
inside the repo you want the changes in.