Skip to content

Migrate backend services to Neon - #43

Open
andrelandgraf wants to merge 6 commits into
JamesSingleton:mainfrom
andrelandgraf:feat/migrate-to-neon
Open

andrelandgraf wants to merge 6 commits into
JamesSingleton:mainfrom
andrelandgraf:feat/migrate-to-neon

Conversation

@andrelandgraf

@andrelandgraf andrelandgraf commented Sep 15, 2026

Copy link
Copy Markdown

Problem

Move Project Aqua's database and object storage from Supabase to Neon while keeping existing accounts, team data and coach workflows usable. Contributors also need one schema migration path and commands for setup, branch development and cutover.

The current application already uses Better Auth and Drizzle through the Next.js backend. Supabase still owns the SQL migration history and logo/avatar storage. Switching connection strings alone would leave those dependencies and existing image URLs behind.

Implementation

The backend uses Drizzle with postgres.js for Lakebase Postgres on Neon and the AWS S3 SDK for Neon Object Storage. Better Auth stays in the application.

Authorization remains in the existing Next.js session, team membership, role and plan checks. On origin/main, the RLS request-binding helpers have no callers outside their definitions and exports; the default database handle falls back to dbAdmin. This PR replaces that wrapper with one db handle and removes the Supabase SQL policies and role setup from the target schema.

Drizzle owns the application schema, migrations and snapshots. The migrations create 52 tables, 31 enums and the 134-event swim catalog. Schema definitions now include details previously held only in SQL, including account.issuer, the one-current-season constraint, relay-result uniqueness with null rounds, the theme check, template timestamps and missing foreign keys and indexes.

Setup and development

Run from the repository root with Node.js 24+ and pnpm 11.22.0:

pnpm install
cp .env.example .env.local
npm i -g neon

neon auth
neon link --no-env-pull
neon deploy
neon env pull --file .env.local

openssl rand -base64 32
# Set BETTER_AUTH_SECRET in .env.local to the generated value.
pnpm db:migrate
pnpm dev:admin

Open http://localhost:3001, sign up and create a team. neon.ts declares the team-logos and user-avatars public-read buckets. The root .env.local supplies database, storage and application configuration to development, build and migration commands.

Variable Use
DATABASE_URL Application connection; takes precedence over the retained POSTGRES_URL fallback
DATABASE_URL_UNPOOLED Preferred migration connection; falls back to the application URL
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY Storage credentials
AWS_ENDPOINT_URL_S3, AWS_REGION Selected branch's storage endpoint and region

Stop the app before switching branches, then restart it to load the new credentials:

neon checkout main
neon checkout dev-meet-import --create
pnpm db:migrate
pnpm dev:admin

After editing packages/db/src/schema/, generate and commit the SQL and snapshots:

pnpm db:generate
pnpm db:migrate
neon diff

Deployment has separate infrastructure, schema and application steps:

neon deploy
pnpm db:migrate
pnpm build

Deploy the Next.js apps through the application host with the selected branch's environment variables. neon deploy applies the bucket configuration; the application host still runs Next.js.

Existing installations

Back up the source database and objects, stop application writes and provision a fresh target through the setup commands above. Keep the existing BETTER_AUTH_SECRET and application URLs. Set SOURCE_DATABASE_URL to the source's direct connection in .env.local, then run:

pnpm db:migrate
pnpm migrate:existing

The importer requires pg_dump, pg_restore and psql. It copies public table data into the Drizzle-created schema and compares every table's row count and content fingerprint. The comparison excludes organization.logo and user.image, which the image step rewrites.

The initial import requires a target without users or teams. A populated target can resume image migration only when its non-image data matches the source. The image step copies referenced logos and avatars into the target buckets and stores /api/storage/ paths. Completed image rows are skipped on a rerun.

Existing images must remain anonymously readable over HTTP(S) and meet the application's image validation rules. Private image origins require an explicit, exact-origin entry in IMAGE_IMPORT_ALLOWED_ORIGINS. Download or validation failures stop the command.

Keep the source read-only until the deployed application's smoke test passes. This is a data cutover into a fresh schema; the command does not upgrade older source schemas or apply Drizzle's initial migration over an existing installation.

Images and existing callers

The roster, meet, workout, attendance and calendar routes retain their existing interfaces. Logo and avatar uploads retain their server-action arguments and result shapes. Their returned image values become same-origin paths:

{ ok: true, logo: "/api/storage/team-logos/<team-id>/<uuid>.png" }
{ ok: true, image: "/api/storage/user-avatars/<user-id>/<uuid>.png" }
GET /api/storage/team-logos/<team-id>/<uuid>.png
HEAD /api/storage/user-avatars/<user-id>/<uuid>.png

The public route serves image bytes from the configured branch endpoint. Successful requests return 200; unknown buckets return 404 and storage HTTP failures retain their upstream status. Responses include a sandboxed content security policy and nosniff.

Logo changes retain the owner/admin/head-coach check. Avatar changes require team membership and update the signed-in user's image. JPEG, PNG, WebP, AVIF and SVG remain supported up to 2 MiB; validation now measures the supplied bytes rather than trusting the optional size field.

Also in here

  • Removes the Supabase client dependency, local configuration and migration directory.
  • Replaces the Supabase agent skills with Neon skills and adds pnpm neon:mcp for project-local Cursor and Claude Code OAuth configuration.
  • Consolidates environment documentation at the root and updates the missing-template-table error to recommend pnpm db:migrate.

Verification

Run against b1b88f7, compared with origin/main at f2b84bb:

pnpm --filter @project-aqua/storage test
# 2 files, 5 tests passed
pnpm --filter @project-aqua/db test
# 7 files, 45 tests passed
pnpm --filter @project-aqua/db --filter @project-aqua/storage --filter @project-aqua/auth check:types
# Passed
bash -n scripts/migrate-existing-installation.sh
node --check scripts/setup-neon-mcp.mjs
# Passed

Added coverage checks encoded object-key round trips, external/other-bucket URL handling, actual image byte limits and download address restrictions. The download tests include a real local HTTP server for an explicitly allowed private origin. git diff --check origin/main...HEAD also passed.

These checks do not establish end-to-end compatibility. No live cutover, storage upload/delete, branch checkout, browser journey or deployment was run for this description. The full build, lint and workspace test suite were not run here.

For your attention

  • The target schema has no RLS policies. Tenant isolation depends on the existing server authorization and query scoping. Cross-team access denial needs live verification before merge.
  • Fresh setup and an existing-installation rehearsal still need verification, including retained sessions, sign-in/out, roster and meet import/export, results, workouts, attendance, calendars and logo/avatar replacement and removal.
  • Database restore is transactional, but database restore and image copying are separate steps. A failed image copy leaves the imported rows in the target so the image step can resume.
  • Only referenced team logos and user avatars are copied. Other source storage objects require a separate migration decision.
  • Development branches retain application data and credentials for external integrations unless configured otherwise. Use sandbox credentials or leave email, billing and calendar credentials empty; those external systems are outside the database/storage branch isolation.

@vercel

vercel Bot commented Sep 15, 2026

Copy link
Copy Markdown

@andrelandgraf is attempting to deploy a commit to the James Singleton's projects Team on Vercel.

A member of the Team first needs to authorize it.

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