Migrate infrastructure to AWS (SST + Fargate + RDS + Clerk + S3 + SES)

✅ merged · #1 · LevelFive-Studio/helix-tribune ← LevelFive-Studio/helix-tribune · opened 13d ago by saratpediredla-level5 · merged 11d ago by saratpediredla-level5 · self · +8,560-6,343 across 73 files · ↗ on GitHub

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)
Email 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

  1. e3ee400 - infra: SST scaffold + Fargate Dockerfile
  2. cea1546 - db: Drizzle schema (16 tables) + initial migration; swap supabase-js dep
  3. fcd1bea - auth: Clerk JWT verification + Drizzle client; first-request profile bootstrap replaces the handle_new_user trigger
  4. 55d189d - routes (E1): simpler routes + access/userSettings/userApiKeys libs to Drizzle
  5. 8d56028 - routes (E2): heavy routes (projects/documents/tabular/chat) + chatTools (3284 lines) to Drizzle; backend tsc --noEmit reaches 0 errors
  6. c599ba5 - storage+email: R2 → S3 in backend/src/lib/storage.ts; Resend → SES helper added
  7. e20c5e9 - frontend auth: <ClerkProvider>, clerkMiddleware, catch-all /login + /signup, all 8 supabase.auth.getSession() call sites + 12 useAuth consumers migrated; 6 dead Supabase files deleted
  8. 01b10ca - frontend deploy: @opennextjs/cloudflare@opennextjs/aws; wrangler removed; Cloudflare-only scripts dropped
  9. 2c0806a - 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 of pg to 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 Supabase handle_new_user trigger.
  • Clerk's users.getUserList({ emailAddress: [...] }) replaces Supabase's auth.admin.listUsers for member lookups in projects.ts and tabular.ts - more efficient than the old "list 1000 users and filter" pattern.
  • backend/schema.sql kept on disk as a no-op file to ease upstream merges (the real schema source is now backend/src/db/schema.ts).
  • nixpacks.toml and frontend/open-next.config.ts left 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 errors
  • npx tsc --noEmit -p frontend: 0 errors
  • npm run lint --prefix frontend: 104 problems - all pre-existing, none introduced by this PR
  • Zero remaining @supabase/* imports anywhere in backend/src or frontend/src

Deployment checklist (post-merge)

  • npm install at repo root (SST CLI)
  • npx sst secret set for every secret declared in sst.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 (or npm run db:migrate --prefix backend with DATABASE_URL pointed at RDS)
  • Configure the Clerk application to issue tokens with email claim 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 deploy to 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_KEY for 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

Replatform helix-tribune from Cloudflare/Supabase to AWS-native 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-1.md from inside the repo you want the changes in.

⬇ Download capture-pull-1.md