codexmachina
Database

Every verified stack backed by MySQL 8.

MySQL 8 via Drizzle ORM and the mysql2 driver.

400 verified stacks400 greenLast updated 2026-08-23How we verify →

400 verified stacks

Showing 12 across 12 app types. Every stack page links its neighbours along each axis, so any combination is a click or two from here.

AI WrapperNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
Blog / CMSNext.js 16 (App Router) · Auth.js (NextAuth)
Booking / schedulingNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
CRMNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
E-commerce storeNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
Fintech ledgerNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
Fitness trackerNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
Forum / communityNext.js 16 (App Router) · Auth.js (NextAuth)
Helpdesk / supportNext.js 16 (App Router) · Auth.js (NextAuth)
IoT telemetryNext.js 16 (App Router) · Auth.js (NextAuth)
Job boardNext.js 16 (App Router) · Auth.js (NextAuth) · Resend
LMS (learning platform)Next.js 16 (App Router) · Auth.js (NextAuth) · Resend

What MySQL 8 gives you

MySQL 8 here is the mysql2 driver under Drizzle's mysql-core dialect: drizzle({ client: pool }) over one module-level mysql.createPool(DATABASE_URL). mysql2's pool multiplexes connections itself, so a single pool per module is the right shape — the runtime and the pool handle concurrency, with no globalThis singleton needed. The framework decides where that file lands (src/lib/db.ts on Next, app/lib/db.ts on React Router, server/lib/db.ts on Nuxt); the client text is the same in all three. The dialect's constraints show up directly in the column types.

MySQL cannot index a TEXT column without a prefix length, so anything that is a primary key, a UNIQUE, an index or a CHECK target is varchar with a declared length: ids are varchar(36) with no database default — the application generates them with crypto.randomUUID(), since there is no uuid type and no defaultRandom() — Better Auth's user.id and every FK pointing at it are varchar(255), an email or slug is varchar(255), a role or status varchar(32), a SHA-256 hex digest varchar(64). Free-form columns nobody indexes stay text. Timestamps are plain timestamp().defaultNow() without the withTimezone flag the Postgres bodies carry, and counters are bigint({ mode: "number" }). The operational difference that matters most: MySQL has no row-level security.

There is no policy layer to fall back on, so multi-tenant isolation is enforced in application code by forOrg(db, orgId) in src/lib/tenant.ts, which wraps each org-owned table's select/update/delete with a where on organization_id and throws on a missing orgId instead of quietly running unscoped. It is a real boundary only while every read and write goes through it — the shared plans catalog sits deliberately outside — and it is app-enforced, not database-enforced. Connections change with the deploy target: connectionLimit 2 per short-lived serverless instance, 10 in a long-running Node process, and on Cloudflare Workers mysql2 cannot run at all — there are no TCP sockets — so the edge client swaps to @planetscale/database over HTTP with drizzle-orm/planetscale-serverless.

Built for

Scope a tenant query

forOrg(db, orgId) from src/lib/tenant.ts returns the org-owned tables (memberships, invitations, subscriptions, api_usage, audit_log, api_keys) pre-filtered on organization_id. A missing orgId throws rather than running an unscoped query.

Key and index a table

varchar(36) primary keys generated by the app, varchar(255) for user_id columns matching Better Auth's id, varchar(32)/(64) for statuses and hashes — because MySQL cannot put an index or a UNIQUE on TEXT without a prefix length.

Constrain a status column

varchar plus a check() constraint from drizzle-orm/mysql-core ("role in ('owner','admin','member')"), mirroring the Postgres body's text + CHECK so both dialects express the same closed set without an enum type.

Size the connection pool

mysql.createPool with connectionLimit tuned per deploy target — 2 for a short-lived serverless instance, 10 for a persistent Node process reusing the pool across requests. The module-level pool is created once, not per request.

Run on Workers

The edge deploy target replaces mysql2 with new Client({ url: DATABASE_URL }) from @planetscale/database plus drizzle-orm/planetscale-serverless, speaking MySQL over fetch — the same schema, a socket-free driver.

Why MySQL 8, specifically

note

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.

note

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.

What MySQL 8 pulls in

bun add drizzle-orm mysql2
DATABASE_URLMySQL connection string (mysql://…)