Helpdesk / support on Nuxt 4, Postgres (Neon) and Better Auth
Type-checked against the real SDKs, migration applied to a live Postgres (Neon), connection clients load-tested, then tracked for upstream drift and re-verified when it moves. How we verify
session validation runs in server components and route handlers, not at the edge
What you're getting
Nuxt 4 (framework mode) — full-stack Vue SSR: client under app/ (Vite), server under server/ (Nitro), API as server/api/*.post.ts Nitro route handlers.
Postgres on Neon via Drizzle ORM and the postgres-js driver.
Better Auth — self-hosted auth running inside your app against your Postgres (Drizzle adapter).
Customer support desk — tickets with status/priority queues, threaded messages with internal notes, agent profiles, and seed-managed SLA policies.
Setup
bun add nuxt vue drizzle-orm postgres better-authDATABASE_URLNeon pooled (-pooler) connection stringBETTER_AUTH_SECRETgenerate with `openssl rand -base64 32`BETTER_AUTH_URLyour app's base URLApply the schema with bunx drizzle-kit push
Initialization
Database client
Helpdesk schema: tickets, messages, agents & SLA policies
Tickets & status/priority queuethe core support request with open/pending/solved/closed status and low/normal/high/urgent priority, indexed for queue views
Ticket messages & internal notesappend-only thread rows on each ticket; is_internal hides agent-only notes from the requester
Agents & team assignmentone agent profile per Better Auth user (unique on user_id), with an optional team field for queue segmentation
SLA policies per priorityseed-managed response and resolution targets in integer minutes, keyed by priority tier
Deploy targets
Decisions and compatibility
Client/server split: the DB client, Drizzle schema, records, and webhooks are server-side (server/). The `@/` alias is the client root (app/); server code reaches shared modules via Nuxt's `~~` rootDir alias (e.g. `~~/server/db/schema`).
prepare: false is mandatory — Neon's pooled endpoint is PgBouncer in transaction mode, where server-side prepared statements break across the pool.
Drizzle is paired here (not Prisma): Prisma's prepared-statement reliance is incompatible with transaction-mode pooling.
Self-hosted: Better Auth owns the user/session/account/verification tables. This stack emits them (db/auth-schema.ts) and hands them to the Drizzle adapter, so app-type schemas can foreign-key `user` directly.
Status and priority are text + CHECK (not pgEnum), so adding a new value (e.g. 'escalated') ships without an ALTER TYPE migration — consistent with the house style in packages/db/src/schema.ts.
agents carries a unique constraint on user_id (one profile per user) and sla_policies carries no unique on priority, allowing multiple named policies at the same priority tier for different customer tiers.