| Component | Supabase dependency | Effort to swap |
|---|---|---|
| Database | None — Drizzle ORM, direct DATABASE_URL |
Zero. Any Postgres works today. |
| Storage | Supabase Storage | Low — interface already abstracted |
| Auth | Supabase Auth | Moderate |
Uses Drizzle ORM with a direct DATABASE_URL connection. No Supabase client for DB queries. Zero work — any Postgres instance works today.
storage.ts already has a StorageProvider interface. You'd just write a LocalFsStorage implements StorageProvider class and swap the singleton. The rest of the codebase never touches Supabase Storage directly. ~1 file to change.
This is the only tightly coupled part. Supabase auth is used in two patterns:
supabase.ts— creates the SSR client (@supabase/ssr)middleware.ts— callssupabase.auth.getUser()to populatelocals.user- 3 auth routes —
signUp(),signInWithPassword(),signOut() - ~11 API routes — all call
supabase.auth.getUser()for auth checks
But the good news is the API routes don't use Supabase auth directly for anything beyond getting the user ID — they all just read locals.user from the middleware, or duplicate the getUser() call. If you centralize auth into the middleware (which it already mostly is), the API routes don't need to change at all.
- A session/JWT system (e.g.
josefor JWTs, or plain cookie sessions) - Password hashing (
argon2) - A
userstable in Postgres (replaces Supabaseauth.users) - Rewrite
supabase.ts→ genericauth.tsthat validates sessions - Rewrite 3 auth routes (login/register/logout)
- Update middleware to use the new auth helper
- Remove
supabase.auth.getUser()calls from API routes that duplicate the middleware check (they should just uselocals.user)
- Storage swap: trivial, interface already exists
- Auth swap: moderate — ~5 files to rewrite, ~11 API routes to clean up (just remove redundant
getUser()calls and rely on middleware) - No schema changes needed —
profilestable already exists in Drizzle