KB-009 — Better Auth + Neon Database Integration: Version Sync, UUID Enforcement, and KV Secondary Storage
KB-009 — Better Auth + Neon Database Integration
Series: Bloqr Compiler Operations & Troubleshooting KB
Component: worker/lib/auth.ts, worker/lib/prisma.ts — Better Auth + Neon via Hyperdrive
Date Created: 2026-06-29
Status: Active
Overview
This article documents the architecture of the Better Auth + Neon database integration in the Bloqr backend worker, the known failure modes encountered during operation (particularly after the monolith-split migration from bloqr-compiler → bloqr-frontend + bloqr-backend), and how to diagnose and fix them.
It also records the dependency upgrades shipped on 2026-06-29 to resolve version skew between the Deno and Node/pnpm environments.
Architecture
flowchart TD
SPA["Browser / Angular SPA"] -->|same-origin proxy via service binding| FE["bloqr-frontend Worker"]
FE -->|env.API.fetch (service binding)| BE["bloqr-backend Worker"]
BE --> BA["Better Auth 1.6.22 (@better-auth/infra 0.3.4)"]
BA --> PRIMARY["Primary storage: Prisma (PostgreSQL adapter) → Neon via Hyperdrive"]
BA --> SECONDARY["Secondary storage: Cloudflare KV (BETTER_AUTH_KV namespace)"]
BE --> PRISMA["Prisma 7.x → @prisma/adapter-pg → pg.Pool → HYPERDRIVE.connectionString"]
Key design decisions
| Decision | Rationale |
|---|---|
| Per-request PrismaClient | Hyperdrive is the connection pool — it proxies locally, so per-request instantiation is safe |
pg.Pool (max: 1) | Cloudflare Workers are stateless isolates — one connection per request is all that’s possible |
| UUID enforcement extension | Prisma $extends query extension replaces non-UUID IDs before PostgreSQL rejects them |
storeSessionInDatabase: true | Better Auth 1.6.x excludes the session model from its schema when secondaryStorage is present unless this flag is set |
disableCSRFCheck: true | Prevents false MISSING_OR_NULL_ORIGIN errors from API/CLI clients; real CSRF protection is sameSite: 'lax' cookies |
Dependency Versions (as of 2026-06-29)
| Package | deno.json | package.json (pnpm) | Notes |
|---|---|---|---|
better-auth | ^1.6.22 | ^1.6.22 | Synced |
@better-auth/infra | ^0.3.4 | ^0.3.4 | Synced; auditLogs not yet exported — see TODO |
@neondatabase/serverless | ^1.1.0 | ^1.1.0 | Synced |
cloudflare | ^6.5.0 | ^6.5.0 | Synced |
@prisma/adapter-pg | ^7.5.0 | ^7.5.0 | Synced |
pg | ^8.21.0 | ^8.21.0 | Synced |
Important:
deno.jsonandpackage.jsonmust be kept in sync. The deployed Cloudflare Worker is bundled by wrangler usingpackage.json/pnpm. Deno tests run againstdeno.json. A version skew causes tests to pass against library behaviour that the production worker does not have.
Known Failure Modes
❶ “BetterAuthError: Model ‘session’ not found in schema”
When: BETTER_AUTH_KV binding is present (KV secondary storage enabled) and storeSessionInDatabase is not set.
Cause: Better Auth 1.6.x excludes the session model from the internal DB schema when secondaryStorage is provided. Without storeSessionInDatabase: true, every sign-in attempt fails with this error.
Fix: Verify AUTH_SESSION_STORE_IN_DATABASE = true is wired into the betterAuth() call’s session.storeSessionInDatabase field. This is enforced as a named constant in worker/lib/auth.ts and tested in worker/lib/auth.test.ts.
export const AUTH_SESSION_STORE_IN_DATABASE = true as const;
// Passed to betterAuth():session: { storeSessionInDatabase: AUTH_SESSION_STORE_IN_DATABASE, ...}❷ “invalid input syntax for type uuid” on sign-up
When: Better Auth generates an opaque alphanumeric ID (e.g. NqEqNgrxWWaQnyBqb9SLtbGG0ODl2TK2) for a User, Session, Account, or other model with a @db.Uuid column.
Cause: Better Auth 1.5.x/1.6.x does not always call advanced.generateId before passing IDs to the Prisma adapter. PostgreSQL’s uuid column type rejects non-UUID strings.
Fix: Two-layer defence:
- Prisma
$extendsquery extension inworker/lib/prisma.ts— intercepts everycreateon Better Auth models and replaces non-UUIDdata.idvalues withcrypto.randomUUID(). advanced.generateIdinworker/lib/auth.ts—AUTH_ID_GENERATOR = () => crypto.randomUUID()covers code paths where Better Auth callsgenerateIddirectly.
The set of guarded models is: User, Session, Account, Verification, TwoFactor, Organization, Member (both PascalCase and lowercase variants, as Prisma 7.x normalises casing inconsistently).
Diagnostic:
# Check for "invalid input syntax for type uuid" in wrangler tail outputwrangler tail bloqr-backend --format=pretty | grep "uuid"❸ Users not visible in the Better Auth Dash dashboard
Cause: BETTER_AUTH_API_KEY and/or BETTER_AUTH_KV_URL secrets are not set on the bloqr-backend worker. The dash() plugin gracefully no-ops when these are absent (no error, but no sync).
Fix:
# Set on bloqr-backend (where Better Auth runs), not bloqr-frontendwrangler secret put BETTER_AUTH_API_KEY --name bloqr-backendwrangler secret put BETTER_AUTH_KV_URL --name bloqr-backend# Value for BETTER_AUTH_KV_URL:# https://api.cloudflare.com/client/v4/accounts/07a1f8d207654fe5d838174af4813126/storage/kv/namespaces/343029e0851240ad8f5799bac38b31bfVerify: After setting secrets and deploying, trigger a sign-up and check the Better Auth Dash dashboard.
❹ Sign-in returns 404 on all /api/auth/* endpoints
Cause: BETTER_AUTH_SECRET is not set on bloqr-backend. The Hono route guard returns c.notFound() (HTTP 404) for every auth request when this secret is absent.
// worker/hono-app.ts — early-return guardif (!c.env.BETTER_AUTH_SECRET) return c.notFound();Fix:
wrangler secret put BETTER_AUTH_SECRET --name bloqr-backend# Generate a value: openssl rand -base64 32Diagnostic:
curl -sS -X POST https://app.bloqr.dev/api/auth/sign-in/email \ -H 'Content-Type: application/json' \ -d '{"email":"test@example.com","password":"wrong"}' \ -w "\nHTTP %{http_code}\n"# 404 → BETTER_AUTH_SECRET not set# 503 → HYPERDRIVE binding missing# 401 or 422 → credential/verification error (auth is working)❺ Sign-in returns 503 (HYPERDRIVE binding missing)
Cause: After the monolith split (bloqr-compiler → bloqr-frontend + bloqr-backend), the new bloqr-backend worker was deployed without the Hyperdrive binding configured, or before the Hyperdrive was provisioned.
Fix:
# Check wrangler.toml has [[hyperdrive]] block# Then verify the ID exists:wrangler hyperdrive list
# Redeploy after adding [[hyperdrive]] to wrangler.tomlwrangler deploy❻ Email verification blocks sign-in for all new users
Cause: RESEND_API_KEY is set (or SEND_EMAIL binding is bound), which enables hasViableEmailProvider, which sets requireEmailVerification: true. If verification emails are not being delivered (e.g. Resend API key is invalid), newly registered users can never sign in.
Fix:
# Check if RESEND_API_KEY is set:wrangler secret list --name bloqr-backend
# If set but broken, test delivery:curl -sS https://app.bloqr.dev/api/auth/sign-up/email \ -H 'Content-Type: application/json' \ -d '{"email":"youraddress@example.com","password":"Test1234!","name":"Test"}' | jq .# Check your inbox for the verification email
# Temporarily disable by removing RESEND_API_KEY if needed:wrangler secret delete RESEND_API_KEY --name bloqr-backendwrangler deploy --name bloqr-backend❼ @neondatabase/serverless / Cloudflare SDK version skew
When: Tests pass in CI (Deno) but production shows different behaviour.
Cause: deno.json and package.json had different versions of @neondatabase/serverless (^0.10.0 vs ^1.1.0) and cloudflare SDK (^5.2.0 vs ^6.3.0). This was resolved on 2026-06-29 — see the upgrade summary below.
Fix: Keep deno.json and package.json versions in sync. The _syncNote field in package.json documents this requirement. After updating either file, run pnpm install to regenerate pnpm-lock.yaml.
Upgrade Summary (2026-06-29)
| Package | Before | After | Files Changed |
|---|---|---|---|
better-auth (deno.json) | ^1.5.6 | ^1.6.22 | deno.json |
better-auth (package.json) | ^1.6.11 | ^1.6.22 | package.json, pnpm-lock.yaml |
@better-auth/infra (both) | ^0.2.13 | ^0.3.4 | deno.json, package.json, pnpm-lock.yaml |
@neondatabase/serverless (deno.json) | ^0.10.0 | ^1.1.0 | deno.json |
cloudflare SDK (deno.json) | ^5.2.0 | ^6.5.0 | deno.json |
cloudflare SDK (package.json) | ^6.3.0 | ^6.5.0 | package.json, pnpm-lock.yaml |
The @neondatabase/serverless package is used only in src/services/neonApiService.ts and scripts/record-deployment.ts via the neon() tagged-template function. The function signature is unchanged between 0.x and 1.x.
The cloudflare SDK (the official Cloudflare REST API TypeScript client) is used only in src/services/cloudflareApiService.ts and scripts/migrate-d1-to-neon.ts via the Cloudflare default export. The SDK is auto-generated from the Cloudflare API spec and the default import pattern is unchanged.
Pending: auditLogs Plugin
@better-auth/infra has planned support for an auditLogs() plugin that records all auth events (sign-in, sign-up, token refresh, role changes, bans, etc.) to the database. As of @better-auth/infra@0.3.4, this export does not exist. The code contains a TODO comment in worker/lib/auth.ts with a placeholder — enable it once the package publishes the export.
Diagnostic Commands
# 1. Check which secrets are set on bloqr-backendwrangler secret list --name bloqr-backend
# 2. Live test — auth endpoint statuscurl -sS -X POST https://app.bloqr.dev/api/auth/sign-in/email \ -H 'Content-Type: application/json' \ -d '{"email":"test@example.com","password":"wrong"}' \ -w "\nHTTP %{http_code}\n"
# 3. Health check — shows per-service status including auth, database, cachecurl -sS https://app.bloqr.dev/health | jq .
# 4. Check Hyperdrive bindingwrangler hyperdrive list
# 5. Tail live worker logswrangler tail bloqr-backend --format=prettyRelated KB Articles
- KB-001 — “Getting API is not available” on the main page (covers
/api/version,/api/clerk-config, etc.) - KB-002 — Hyperdrive binding connected but
databaseservice reportsdown - KB-003 — Database Down After Deploy — Live Debugging Session (2026-03-25)
- KB-005 — Better Auth Cloudflare: Worker CPU timeout & rate limiting skipped
Feedback & Contribution
If you discover a new failure mode while using this article, open an issue tagged troubleshooting and documentation in bloqr-systems/bloqr-compiler with details so it can be captured in a follow-up KB entry.