Skip to content

Decide the post-Supabase provider set (Postgres / auth / storage / email) #3

Description

@AlaskanTuna

Supabase supplies four separate things — Postgres, Auth/SSO, Storage, and (indirectly) transactional email via a Resend SMTP relay configured inside Supabase Auth. Exiting means choosing four replacements, and the choices are coupled: the auth provider determines whether user UUIDs survive, which determines whether Project.userId needs a backfill, which determines the order of tasks 5 and 7. This is a decision task — its deliverable is a recorded decision in docs/decisions.md, not code. The recommendation below is researched but must be confirmed by the human at Gate 1, because it commits the project to a specific vendor set for the foreseeable future.

Researched recommendation, with the disqualifying facts that drove it:

Concern Recommendation Why, and what was ruled out
Postgres Neon free plan Neon's free plan has no inactivity-based pause, archival, or deletion — the only idle behaviour is compute scale-to-zero after 5 minutes, which resumes automatically on the next connection. That is precisely the failure being escaped. Limits: 0.5 GB storage/project, 100 CU-hours/month, 5 GB transfer/month; exceeding any of these suspends compute until the next monthly window (data is never deleted). Render Postgres is disqualified — free databases expire 30 days after creation and are deleted after a 14-day grace period. Aiven pauses inactive databases. Xata's managed free database is removed after a set period.
Storage Cloudflare R2 10 GB storage, 1M Class A + 10M Class B operations/month, $0 egress, permanently free, no idle expiry. S3-compatible, so the swap is an @aws-sdk/client-s3 drop-in behind the three functions in storageService.ts. Preserves the one-Solar-API-call-per-location cache constraint exactly.
Auth Better Auth, self-hosted in the Express backend, tables in the same Neon database via the Prisma adapter The decisive property is user-ID preservation: Better Auth's user table takes a String id, so the rescued Supabase UUIDs can be inserted verbatim and every Project.userId FK survives with zero backfill. It supports email+password with requireEmailVerification, Google OAuth via socialProviders, and a user-supplied sendVerificationEmail callback that wires straight to Resend. No MAU cap, no vendor that can change its free tier, no idle pause. Alternative worth considering: Clerk (free to 50k MRU as of Feb 2026, far less engineering) — but Clerk mints its own user_... ids, forcing either a mapping table or a Project.userId migration, and re-introduces exactly the "someone else's free tier" dependency this phase exists to remove.
Email Resend, called directly from the backend Already the underlying sender; today it is merely relayed through Supabase Auth's SMTP config. solarsim.tech is already verified in Resend with DKIM, SPF, and an MX bounce route (per RUNBOOK.md), and RESEND_API_KEY is already in .env.example. Removing the Supabase relay is therefore a simplification, not a new integration — no DNS work.

Scope: docs/decisions.md, docs/trd.md

Checklist

  • Re-verify each provider's current free-tier terms at decision time rather than trusting this table — free-tier terms move fast and this plan was written 2026-07-29
  • Confirm the Neon free-plan headroom against real numbers from the task 1 rescue: total public schema size must fit 0.5 GB (GeoTIFFs live in object storage, not Postgres, so this should be comfortable — confirm, don't assume)
  • Compute the CU-hour budget: Neon free is 100 CU-hours/month and free-plan compute floors at 0.25 CU, giving roughly 400 hours of active compute. Record the consequence explicitly: a keep-alive pinger that touches the database will hold compute awake ~730 h/month ≈ 180 CU-hours and suspend the project. This constrains task 10's cold-start mitigation.
  • Confirm the total rescued storage-object footprint fits R2's 10 GB free tier
  • Spike Better Auth for one hour against a throwaway Neon database: insert a user row with a hand-chosen UUID, sign in with email+password, and confirm the id round-trips into the session — this is the single assumption the whole auth plan rests on
  • Decide and record: if the spike fails, fall back to Clerk and accept a Project.userId backfill migration as an added task
  • Append one line per settled choice to docs/decisions.md (four lines: Postgres, storage, auth, email)
  • Update the infrastructure section of docs/trd.md to name the new providers — docs/trd.md is canonical for architecture and must not be left describing Supabase
  • verify: docs/decisions.md contains four dated one-line entries, each with a rationale clause
  • verify: the Better Auth spike's custom-UUID result is pasted into the GitHub issue as evidence, not asserted

Priority: Critical · Phase 12 task 4 of the maintainer's migration plan.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0-criticalApp is down / blocks everything elsemigrationMoving off a provider

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions