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.
Bloqr AI™ — The privacy you didn't know you needed.
© 2026 Bloqr AI™, a trademark of Bloqr Systems™. Created by Bloqr Systems™, founded by Jayson Knight.
Internet Hygiene (n.) — the ongoing practices that keep your digital life clean, private, and safe.
Our product repos live in the BloqrAI org, part of the Bloqr Systems GitHub Enterprise. Product repos are internal-visibility — enterprise membership is required to view them.