Forum / community on React Router v8, 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
React Router v8 (framework mode) — SSR, config/file routes under app/, loaders/actions, and resource routes for API endpoints.
MySQL 8 via Drizzle ORM and the mysql2 driver.
Better Auth — self-hosted auth running inside your app against your Postgres (Drizzle adapter).
Forum / community — slug-keyed category taxonomy, threaded discussions with open/locked status and pinning, and per-reply up/down votes.
Setup
bun add react-router react react-dom 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
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
Deploy targets
Decisions and compatibility
Framework mode (not data/library mode): routes live under app/, declared in app/routes.ts. API endpoints are resource routes (a route module exporting loader/action but no default component).
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.
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.
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.