Notes / knowledge base on Nuxt 4, MySQL 8 and Better Auth
Type-checked against the real SDKs, migration applied to a live MySQL 8, 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.
MySQL 8 via Drizzle ORM and the mysql2 driver.
Better Auth — self-hosted auth running inside your app against your Postgres (Drizzle adapter).
Notes / knowledge-base — user-owned notebooks, versioned notes, free-form tags, and per-note read/write sharing.
Setup
bun add nuxt vue drizzle-orm mysql2 better-authDATABASE_URLMySQL connection string (mysql://…)BETTER_AUTH_SECRETgenerate with `openssl rand -base64 32`BETTER_AUTH_URLyour app's base URLApply the schema with bunx drizzle-kit push
Initialization
Database client
Notes app schema: notebooks, notes, tags & sharing
Notebooks & notesthe owner-scoped notebook containers and the notes nested inside them, with archive support
Tagsfree-form text labels attached to individual notes, deduplicated by a composite unique constraint
Sharing & permissionsper-note grants to other users with a CHECK-enforced read/write permission level
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`).
mysql2's pool multiplexes connections; drizzle-orm/mysql2 wraps it. One module-level pool is right for a serverless/edge app — the runtime and the pool handle concurrency.
MySQL has no row-level security: multi-tenant isolation is enforced in application code via the forOrg helper (src/lib/tenant.ts), not by the database. See the tenant-scoping section on SaaS pages.
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.
note_tags enforces a unique constraint on (note_id, tag), so duplicate tags per note are rejected at the DB level rather than in application code.
note_shares carries a CHECK constraint limiting permission to 'read' or 'write', and a composite unique on (note_id, shared_with) — one share row per user per note, updated in place rather than appended.
MySQL provides no row-level security. On MySQL, multi-tenant isolation is APP-ENFORCED via the forOrg helper (src/lib/tenant.ts), not database-enforced like Postgres RLS. Every org-scoped query MUST go through forOrg — a missed query leaks across tenants. Postgres cells enforce this in the database itself (RLS), so it holds even for a query that forgets to scope.