Forum / community on Next.js 16 (App Router), MySQL 8 and Clerk
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
Next.js 16 App Router — file-based routing, server components, and the Edge proxy (Next 16's renamed middleware).
MySQL 8 via Drizzle ORM and the mysql2 driver.
Clerk — hosted identity (sign-in UI, sessions, user management) mounted via middleware + provider.
Forum / community — slug-keyed category taxonomy, threaded discussions with open/locked status and pinning, and per-reply up/down votes.
Setup
bun add next react react-dom drizzle-orm mysql2 @clerk/nextjsDATABASE_URLMySQL connection string (mysql://…)NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEYCLERK_WEBHOOK_SECRETsvix secret that verifies Clerk webhook signaturesApply the schema with bunx drizzle-kit push
Initialization
Database client
Forum / community schema: categories, threads, replies & votes
Categories taxonomyslug-unique top-level categories each thread must belong to exactly one of
Threads (status & pinning)threads scoped to a category and author, with open/locked status and an is_pinned flag for surfacing
Thread repliesflat replies within a thread, each carrying a body and an author FK into Better Auth's user table
Reply votesappend-style up/down votes on replies, uniquely constrained per (reply, voter) pair
Verified identity sync (Clerk)
Deploy targets
Decisions and compatibility
Auth runs in proxy.ts (Next 16's renamed middleware) on the Edge runtime: it gates on the session cookie's presence only — full session validation happens in Server Components and route handlers, not in the proxy.
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.
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`.
Vote identity is enforced by a composite unique on (reply_id, voter_id): one vote per reply per voter, value constrained to (-1, 1) via CHECK rather than a separate enum.
Thread status uses text + CHECK ('open','locked') so new statuses ship without an ALTER TYPE migration; the same pattern applies to vote value.
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.
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.