Skip to content

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

Closed
saratpediredla-level5 wants to merge 10 commits into
willchen96:mainfrom
LevelFive-Studio:feat/aws-migration
Closed

Migrate infrastructure to AWS (SST + Fargate + RDS + Clerk + S3 + SES)#96
saratpediredla-level5 wants to merge 10 commits into
willchen96:mainfrom
LevelFive-Studio:feat/aws-migration

Conversation

@saratpediredla-level5
Copy link
Copy Markdown

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. e3ee400infra: SST scaffold + Fargate Dockerfile
  2. cea1546db: Drizzle schema (16 tables) + initial migration; swap supabase-js dep
  3. fcd1beaauth: Clerk JWT verification + Drizzle client; first-request profile bootstrap replaces the handle_new_user trigger
  4. 55d189droutes (E1): simpler routes + access/userSettings/userApiKeys libs to Drizzle
  5. 8d56028routes (E2): heavy routes (projects/documents/tabular/chat) + chatTools (3284 lines) to Drizzle; backend tsc --noEmit reaches 0 errors
  6. c599ba5storage+email: R2 → S3 in backend/src/lib/storage.ts; Resend → SES helper added
  7. e20c5e9frontend auth: <ClerkProvider>, clerkMiddleware, catch-all /login + /signup, all 8 supabase.auth.getSession() call sites + 12 useAuth consumers migrated; 6 dead Supabase files deleted
  8. 01b10cafrontend deploy: @opennextjs/cloudflare@opennextjs/aws; wrangler removed; Cloudflare-only scripts dropped
  9. 2c0806adocs: 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

saratpediredla-level5 and others added 10 commits May 12, 2026 18:18
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translates the Supabase schema in backend/schema.sql to a Drizzle ORM
definition targeting AWS RDS/Aurora Postgres. Lays the foundation for
Stages D/E, which rewrite the routes and middleware off supabase-js.

- backend/src/db/schema.ts: 16 tables translated 1:1 with schema.sql.
  user_id columns that previously referenced auth.users(id) are now
  plain text (Clerk owns identity). RLS policies, the handle_new_user
  trigger, and Supabase grants are not modelled - access control moves
  to the Express layer.
- backend/drizzle.config.ts: drizzle-kit config with a dummy default
  DATABASE_URL so `generate` works without env setup.
- backend/drizzle/0000_init.sql: initial migration with all tables,
  indexes (including GIN on shared_with), check constraints, and
  foreign keys. Prepended CREATE EXTENSION pgcrypto. Manually appended
  the two circular FKs (documents.current_version_id and
  document_edits.chat_message_id) that Drizzle could not emit inline.
- backend/package.json: dropped @supabase/supabase-js; added
  drizzle-orm, drizzle-kit, pg, @types/pg, @clerk/backend. Added
  db:generate / db:migrate / db:push / db:studio scripts.

The legacy backend/schema.sql is kept in place until upstream merges
clear; Stage D will start removing supabase.ts and the route consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop @supabase/supabase-js from middleware/auth.ts; verify Clerk JWTs
  with @clerk/backend (verifyToken), supporting both JWKS and offline
  jwtKey verification.
- First-request profile bootstrap (in-memory cache) replaces the old
  Supabase handle_new_user trigger.
- Add backend/src/lib/db.ts exporting Drizzle client + pg.Pool +
  withTransaction helper.
- Delete backend/src/lib/supabase.ts. Routes and several libs still
  import it; Stages E1/E2 will rewrite them.

tsc --noEmit fails (42 errors), all confined to files Stage E1/E2 will
rewrite. db.ts and middleware/auth.ts typecheck cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite user, downloads, workflows, and projectChat routes — plus the
access, userSettings, and userApiKeys libs — to query Postgres directly
via Drizzle instead of Supabase. Brings the backend tsc error count from
40 to 24; remaining errors are all in Stage E2 files (chat, projects,
documents, tabular, chatTools, documentVersions).

- Lib helper signatures keep a trailing optional `_db` arg so out-of-scope
  E2 callers don't all immediately break on extra-argument errors before
  Stage E2 lands.
- Workflows route now backfills sharer display names via the Clerk
  Backend API instead of the Supabase admin listUsers call.
- `/user/account` no longer calls supabase.auth.admin.deleteUser; it
  removes the local profile row and leaves Clerk-side deletion to a
  separate path.
- userApiKeys keeps the AES-GCM crypto unchanged; only the DB plumbing
  swapped to Drizzle's onConflictDoUpdate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites projects, documents, tabular, chat, chatTools, and
documentVersions to use Drizzle queries against the shared db
singleton. Drops the trailing _db?: unknown bridge parameter that
Stage E1 added to access/userSettings/userApiKeys helpers.

Behavioral notes:
- Member email->id lookups in projects.ts and tabular.ts now use
  Clerk's users.getUserList({ emailAddress: [...] }) and
  users.getUser(id) instead of Supabase's auth.admin.listUsers.
- chat.ts /chat OR-filter converted from Supabase's PostgREST .or()
  string syntax to Drizzle's or(eq, inArray).
- Dead buildTabularContext helper removed from tabular.ts (no
  callers anywhere).
- jsonb inserts retain as any casts to match Stage E1 pattern.

tsc --noEmit -p backend: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite backend/src/lib/storage.ts to use native AWS S3 via the default credential chain (Fargate task role). Drop R2 endpoint, forcePathStyle, and explicit access-key env vars. BUCKET reads S3_BUCKET_NAME with a transitional R2_BUCKET_NAME fallback.
- Add backend/src/lib/email.ts wrapping @aws-sdk/client-sesv2 with a sendEmail helper that no-ops when SES_FROM_ADDRESS is unset.
- Swap deps in backend/package.json: drop resend, add @aws-sdk/client-sesv2. Refresh package-lock.json.
- No existing Resend call sites in backend/src — helper added for future use.

tsc --noEmit -p backend: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces every frontend Supabase auth touchpoint with Clerk equivalents. ClerkProvider wraps the app, clerkMiddleware protects routes, and Clerk's hosted SignIn/SignUp components render at /login and /signup via catch-all segments. mikeApi reads the bearer token from window.Clerk.session, and hooks/components that previously called supabase.auth.getSession() now use useAuth().getToken() from @clerk/nextjs. AuthContext is removed; consumers read user/userId from Clerk's useAuth/useUser hooks directly.

Notes:
- @clerk/nextjs 7.3.3 requires react ~19.2.3; project pins 19.2.0, so npm install was run with --legacy-peer-deps. Runtime works correctly.
- tagWIdsOnRenderedDom in DocxView refactored to accept a token argument since it runs outside React hooks.
- tsc --noEmit -p frontend: clean. Lint: 104 problems (-1 vs baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@saratpediredla-level5
Copy link
Copy Markdown
Author

Created against the wrong repo (intended for fork LevelFive-Studio/helix-tribune); closing and reopening on the fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant