Lean WoW private-server CMS in TypeScript. Replaces FusionCMS for people who
don't want to run PHP. Targets TrinityCore, AzerothCore, and cMaNGOS
via a single EmulatorAdapter interface.
Stack: Bun · SvelteKit · TailwindCSS · Drizzle ORM · mysql2 · Stripe · Zod.
- Public registration + login (SRP6 v1 and legacy SHA1, multi-realm)
- Account dashboard: change password / email, character list
- Public armory: search + character profile
- Admin: realms CRUD, account search, ban/unban, send in-game mail with item attachments, audit log
- Stripe donation page (records intent — manual fulfillment, by design)
Out of scope: plugin system, vote-rewards, item shop, i18n, forums, Battle.net auth, transmog viewer.
Assumes an emulator stack (TrinityCore / AzerothCore / cMaNGOS) is running with
its MySQL reachable. The CMS metadata DB (wow_cms) sits beside the emulator's
auth / characters / world.
bun install
cp .env.example .env
# edit CMS_DB_URL — point at a MySQL user that can write to `wow_cms`
# e.g. CMS_DB_URL=mysql://root:<rootpw>@127.0.0.1:3306/wow_cms
bun run db:push # creates the 30 CMS tables (idempotent)
bun run devOpen http://localhost:5173. First visit redirects to /setup — a one-page
wizard that tests your emulator MySQL, creates the realm row, creates the first
admin (real SRP6 account, usable in-game), and logs you into /admin.
CLI alternative for scripted installs:
bun run db:seed:dev # CREATE DATABASE wow_cms + insert dev realm
bun run db:make-admin <user> # promote a cms_user to adminCMS has its own user table (cms_user) backlinked to the emulator's
auth.account row by (realm_id, account_id). One password — the one the
emulator already stores via SRP6 (Trinity / AzerothCore) or SHA1 (cMaNGOS). The
CMS doesn't store passwords; it verifies against the emulator's hash on every
login.
Registration is dual-write: auth.account first, then cms_user. If the
emulator row exists already, the CMS row is created lazily on first login —
self-healing, no orphans.
CMS admin and in-game GM rank are independent. A CMS-only moderator may
have no in-game powers; a senior GM may have no CMS access. Adapters expose
getGmLevel(accountId) so the admin UI can display in-game rank as context,
but it never feeds into authorization. Promote manually with
bun run db:make-admin <username>.
The only test that proves SRP6 actually works:
/register→ createtestuser/testpass.- WoW 3.3.5 client → connect to
127.0.0.1→ log in astestuser. Must succeed. - Make a character, log out.
- CMS armory → look up that character. Profile renders.
- Admin → Send Mail → recipient = your character, items =
49623:1(Shadowmourne). - Log back in-game, mail tab shows the item.
- Admin → Users → Search → Ban. Next login attempt rejected.
src/lib/server/emulator/ holds the adapter interface, factory (realm row →
cached adapter), and per-emulator implementations (trinity.ts,
azerothcore.ts, mangos.ts). Hash logic lives in hash/ (SRP6 v1 with
bigint modPow + LE byte order; SHA1(UPPER(user):UPPER(pass)) for legacy).
db/ holds Drizzle schema + pool. auth/ holds session cookies and Zod
registration. acl.ts is the role + permission catalog. SvelteKit routes
under src/routes/ are SSR-only.
your TrinityCore / AzerothCore / cMaNGOS MySQL
├── auth ◄── owned by emulator, read+write by EmulatorAdapter (mysql2)
├── characters ◄── owned by emulator, read by EmulatorAdapter
├── world ◄── owned by emulator, read by EmulatorAdapter
└── wow_cms ◄── owned by Drizzle (this project)
Drizzle never touches auth / characters / world. The adapter never
touches wow_cms. Two non-overlapping ownership zones.
- Implement
EmulatorAdapterinsrc/lib/server/emulator/adapters/<name>.ts. - Register it in
factory.ts. - Add the kind to
EmulatorKindintypes.tsand themysqlEnumonrealm.emulatorinschema.ts.
Login, register, admin pages are adapter-agnostic.
No separate Docker DB for the CMS — wow_cms lives inside your existing
emulator MySQL. Drizzle owns that schema; emulator DBs are read/written only by
EmulatorAdapter.
If your emulator MySQL lives elsewhere than CMS_DB_URL, override per-DB URLs
on the dev realm row via DEV_AUTH_DB_URL / DEV_CHAR_DB_URL / DEV_WORLD_DB_URL.
MOCK_DB=true short-circuits Drizzle + adapter for offline UI smoke tests.
MOCK_DB=false runs the real path. Don't commit .env.
Inline temporary code prefixed // DEV-FIXTURE: — grep before shipping.
Modules and runtime theme switching are ~40% of FusionCMS's complexity and ~5%
of the user value. Fork the repo to change look-and-feel. A "module" is just
src/routes/<thing>.
MIT.