Fintech ledger on Nuxt 4, Postgres (Neon) and Clerk
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.
Clerk — hosted identity (sign-in UI, sessions, user management) mounted via middleware + provider.
Double-entry fintech ledger — chart of accounts, immutable journal entries, and debit/credit lines with integer-cent amounts.
Setup
bun add nuxt vue drizzle-orm postgres @clerk/nextjsDATABASE_URLNeon pooled (-pooler) connection stringNEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEYCLERK_WEBHOOK_SECRETsvix secret that verifies Clerk webhook signaturesApply the schema with bunx drizzle-kit push
Initialization
Database client
Double-entry ledger schema: accounts, journal entries & lines
Chart of accountsper-owner accounts typed to one of five standard categories (asset/liability/equity/revenue/expense)
Journal entriesimmutable header rows tying a description and posted timestamp to a creator
Debit/credit lines & balancesthe individual debit/credit lines linking each journal entry to an account with an integer-cent amount
Verified identity sync (Clerk)
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.
Hosted: Clerk owns identity and does NOT create a local `user` table. Store `clerk_user_id` as text without a foreign key, or sync Clerk users into a local table via webhook before relying on FKs to `user`.
The accounts table constrains type to the five canonical categories (asset/liability/equity/revenue/expense) via a DB CHECK — adding a new account class requires a migration, not just app code.
Debit/credit balance (sum of debits == sum of credits per entry) is enforced in application logic, not at the DB level; the schema's CHECK only guards direction values ('debit'/'credit'), so the invariant can be violated if bypassed.
Clerk is a hosted identity provider and does not create a local `user` table. This schema's foreign keys to `user` assume a local identity table (as Better Auth provides). With Clerk, store `clerk_user_id` as a text column without a foreign key, or sync Clerk users into a local `users` table via webhook before relying on these FKs.