Skip to content

Neon PostgreSQL Migration — Complete Summary

Neon PostgreSQL Migration — Complete Summary

This document provides a comprehensive record of the migration from Cloudflare D1 (SQLite) to Neon PostgreSQL as the primary database for bloqr-backend. It covers the motivation, scope, technical decisions, implementation details, CI/CD changes, and post-migration improvements.

For topic-specific guides, see the cross-references at the end of this document.

Background

GitHub Issue #1257 requested migrating from Cloudflare D1 to Neon PostgreSQL, integrating Better Auth as the primary authentication provider (replacing Clerk), and adopting Prisma ORM for type-safe database access throughout the codebase.

Motivation

ConcernD1 (Before)Neon PostgreSQL (After)
SQL dialectSQLitePostgreSQL
Connection poolingN/A (embedded)Cloudflare Hyperdrive
ORM supportRaw SQL / partial PrismaFull Prisma with typed client
Auth providerClerk (third-party SaaS)Better Auth (self-hosted, OSS)
Branching / preview DBsNot supportedNeon branch-per-PR via GitHub Actions
Point-in-time recoveryManual backupsNative Neon PITR

Design Decisions

These decisions were confirmed with the project owner before implementation began:

DecisionChoiceRationale
Primary auth providerBetter AuthSelf-hosted, OSS, Prisma-native adapter, Cloudflare-compatible
Clerk dispositionDisabled via DISABLE_CLERK_FALLBACK=trueKept as fallback code path during transition; can be removed later
D1 dispositionRetained as L1 edge cacheLow-latency reads at the Cloudflare edge; write-through to Neon
Connection methodCloudflare HyperdriveTCP connection pooling at the edge, built-in to Workers runtime
ORM strategyPrisma everywhereType-safe queries, generated client, migration management
Local development DBNeon by default; Docker PostgreSQL as opt-in alternativeSeamless parity with production; Docker for offline/isolated work

Scope

PR #1264 — Core Migration (Merged)

  • Commit: 289b3cbf0
  • Stats: 104 files changed, +18,847 insertions, -1,424 deletions
  • Branch: feat/neon-migration-1257 (deleted after merge)

PR #1266 — DX Improvements (Open)

  • Branch: fix/dx-improvements
  • Stats: 5 commits, focused on CI fixes and developer experience

Database Schema

The Prisma schema defines 14 models across three domains:

Authentication (5 models)

ModelPurpose
UserUser accounts with email, name, image, role, email verification status
SessionActive sessions with expiry, IP address, user agent tracking
AccountOAuth/credential provider links (Better Auth multi-provider support)
VerificationEmail/phone verification tokens with expiry
ApiKeyHashed API keys with optional expiry, tier assignment, last-used tracking

Compiler Domain (5 models)

ModelPurpose
FilterSourceRegistered filter list source URLs with format, health status, scheduling
FilterListVersionVersioned snapshots of downloaded filter list content
CompiledOutputCompilation results with format, rule count, hash, R2 storage key
CompilationEventAudit log of compilation runs with timing, status, trigger source
CompilationMetadataExtended key-value metadata attached to compilation events

Infrastructure (4 models)

ModelPurpose
SourceHealthSnapshotPeriodic health checks for filter sources (latency, status, size)
SourceChangeEventChange detection events when filter list content differs from prior version
StorageEntryGeneric key-value storage with optional TTL
FilterCacheCached filter list data with TTL and ETag support

The full schema is defined in prisma/schema.prisma. A single initial migration (prisma/migrations/20260322030000_init/migration.sql) creates all 14 tables.

For field-level documentation, see Prisma Schema Reference.

Authentication Architecture

The migration introduced a four-tier authentication chain, evaluated in order for each request:

flowchart TD
    A[Incoming Request] --> B{API Key header?}
    B -- Yes --> C[Validate API Key in DB]
    C -- Valid --> D[Auth: api_key tier]
    C -- Invalid --> E[401 Unauthorized]
    B -- No --> F{Better Auth session cookie?}
    F -- Yes --> G[Validate session via Prisma]
    G -- Valid --> H[Auth: better_auth tier]
    G -- Invalid --> I[Continue chain]
    F -- No --> I
    I --> J{Clerk JWT present?}
    J -- Yes --> K{DISABLE_CLERK_FALLBACK?}
    K -- false --> L[Validate Clerk JWT]
    L -- Valid --> M[Auth: clerk tier]
    L -- Invalid --> N[Continue chain]
    K -- true --> N
    J -- No --> N
    N --> O[Auth: anonymous tier]

The AuthFacadeService on the Angular frontend abstracts the active provider, allowing runtime switching without component changes.

For the full auth chain specification, see Auth Chain Reference.

Infrastructure Changes

Cloudflare Hyperdrive

All Worker-to-Neon connections route through Cloudflare Hyperdrive, which provides:

  • TCP connection pooling at the edge (no cold-start connection overhead)
  • Automatic connection reuse across Worker isolates
  • Transparent TLS termination

Configuration is in wrangler.toml under [[hyperdrive]]. The binding is accessed as env.HYPERDRIVE in Worker handlers.

GitHub Actions Workflows

Two new workflows support Neon database branching for pull requests:

neon-branch-create.yml — Runs on PR open/synchronize:

  1. Creates a Neon branch from main (copy-on-write, near-instant)
  2. Installs pnpm and Node.js
  3. Runs prisma migrate deploy against the branch
  4. Outputs the pooled connection URL as a PR environment variable

neon-branch-cleanup.yml — Runs on PR close:

  1. Deletes the Neon branch to free resources

Both workflows use neondatabase/create-branch-action@v5, which is a composite action wrapping the neonctl CLI. Key input/output mappings for v5:

InputPurposeThis project
usernameDatabase role nameDerived from NEON_DATABASE_URL
databaseDatabase nameDerived from NEON_DATABASE_URL (bloqr-backend)
branch_nameName for the new branchpr-<number>
parentParent branch to fork fromproduction
OutputPurpose
db_url_with_poolerPooled connection string
db_urlDirect connection string
branch_idNeon branch identifier

Docker Local Development

A docker-compose.yml service provides an optional local PostgreSQL 16 instance:

Terminal window
deno task db:local:up # Start PostgreSQL with healthcheck --wait
deno task db:local:push # Push Prisma schema to local DB
deno task db:local:studio # Open Prisma Studio against local DB
deno task db:local:reset # Destroy and recreate local DB
deno task db:local:down # Stop PostgreSQL

The default local development path uses the Neon cloud database directly (via DATABASE_URL in .env.local). Docker is available for offline or isolated development.

Environment Configuration

Required Variables

VariableLocationPurpose
DATABASE_URL.env.localNeon pooled connection string (used by Prisma)
DIRECT_DATABASE_URL.env.localNeon direct connection string (used by migrations)
BETTER_AUTH_SECRET.dev.varsHMAC secret for Better Auth session signing
BETTER_AUTH_URL.dev.varsBase URL for Better Auth endpoints
DISABLE_CLERK_FALLBACK.dev.varsSet to true to disable Clerk JWT validation
HYPERDRIVEWorker bindingCloudflare Hyperdrive binding (configured in wrangler.toml)

Environment Files

FilePurposeCommitted
.env.exampleTemplate with all variables documentedYes
.dev.vars.exampleTemplate for Cloudflare Worker secretsYes
.env.localLocal development valuesNo (gitignored)
.dev.varsLocal Worker secret valuesNo (gitignored)

One-Command Setup

Terminal window
deno task setup

This copies example env files, generates the Prisma client, and installs git hooks.

Deno Task Reference

All database-related tasks:

TaskPurpose
deno task setupOne-command project bootstrap
deno task db:generateGenerate Prisma client (with Deno import fix)
deno task db:pushPush schema to Neon (no migration file)
deno task db:migrateCreate and apply migration (development)
deno task db:migrate:deployApply pending migrations (CI/production)
deno task db:studioOpen Prisma Studio GUI
deno task db:local:upStart Docker PostgreSQL
deno task db:local:downStop Docker PostgreSQL
deno task db:local:resetDestroy and recreate Docker PostgreSQL
deno task db:local:pushPush schema to Docker PostgreSQL

All Prisma tasks use --env=.env.local to load environment variables. The db:generate task runs scripts/prisma-fix-imports.ts afterward to rewrite .js import specifiers to .ts for Deno compatibility.

Migration Script

A one-time D1-to-Neon data migration script is available:

Terminal window
deno task db:migrate:d1-to-neon -- --dry-run # Preview without writing
deno task db:migrate:d1-to-neon # Execute migration

The script reads from D1 (via Cloudflare API), transforms data to match the Prisma schema, and writes to Neon via Prisma Client. It includes row-count verification and can be run multiple times safely (idempotent upserts).

For the full migration checklist, see Migration Checklist.

CI/CD Pipeline

The CI pipeline validates the full stack on every push:

CheckWhat It Validates
Type Checkdeno task check — Deno + Worker type correctness
Testdeno task test — 2,557+ backend tests
Frontend (build)ng build — Angular production build
Frontend (lint, test)ng lint && ng test — 950+ frontend tests
Lint & Formatdeno lint && deno fmt --check
Security ScanTrivy vulnerability scanner
Check Slow Typesdeno publish --dry-run — JSR compatibility
Validate Migrationsscripts/validate-migrations.ts
Validate Generated ArtifactsSchema/Postman drift detection
Create Neon BranchPer-PR database branch with Prisma migration
Verify Worker BuildCloudflare Workers build verification
ZTA LintZero Trust Architecture compliance
CodeQLStatic analysis (JavaScript/TypeScript + Actions)

Post-Migration Improvements (PR #1266)

After the core migration merged, a follow-up PR addressed developer experience gaps:

  1. Environment validation guardsworker/lib/auth.ts and prisma.config.ts throw descriptive errors when required bindings or variables are missing, replacing cryptic runtime failures
  2. Global error handlerapp.onError() in worker/hono-app.ts catches unhandled exceptions and returns structured JSON with a requestId, preventing stack trace leaks
  3. Neon CI workflow fixes — Corrected action input names for neondatabase/create-branch-action@v5, fixed pnpm/Node.js step ordering, and resolved pnpm version conflict with packageManager field
  4. Setup taskdeno task setup provides one-command project bootstrap
  5. Docker improvementsdb:local:up and db:local:reset use --wait flag for reliable healthcheck-based startup

Cross-References

TopicDocument
Neon production setupNeon Setup
Neon branch-per-PR workflowsNeon Branching
D1-to-Neon data migration checklistMigration Checklist
Database architecture (D1 cache + Neon primary)Database Architecture
Prisma + Deno compatibility notesPrisma Deno Compatibility
Prisma schema field referencePrisma Schema Reference
Local development setupLocal Dev
Better Auth + Prisma integrationBetter Auth Prisma
Clerk → Better Auth migration guideAuth Migration
Auth chain runtime behaviorAuth Chain Reference
End-user migration guideUser Migration Guide
Production secrets managementProduction Secrets
Disaster recovery proceduresDisaster Recovery
Developer onboardingDeveloper Onboarding
Neon troubleshootingNeon Troubleshooting
Database testing patternsDatabase Testing