- Layered API (router -> controller -> model -> db), serverlinkr pattern - Public / auth / admin route groups; posts, wiki, settings, users, activity models - JWT httpOnly-cookie auth (Secure auto-detected: LAN HTTP + Pangolin HTTPS) - Site LIVE/MAINTENANCE mode with admin preview bypass - Dual file+console logging (info/warn/error/debug) + HTTP access logs - Docker Compose (app + MariaDB), schema.sql + seed, .env.example - Verified end-to-end against MariaDB (27/27 smoke checks) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.6 KiB
UOMysticmoon Website
Public site, wiki, and protected admin panel for the UOMysticmoon private Ultima Online
shard. Built on the serverlinkr layered pattern: Express + MariaDB + JWT backend and a
React + Vite frontend in the same repo, deployed with Docker Compose behind a
Pangolin reverse proxy.
Build order: (1) backend (this phase) → (2) frontend design (Claude Design) → (3) frontend coding. See BACKEND_DESIGN.md for the full design.
Layout
server/ Express API (router → controller → model → db), MariaDB schema + seed
client/ React + Vite SPA (added in the frontend phase)
Dockerfile, docker-compose.yml, .env.example
Quick start (local dev)
# 1. Start a MariaDB (or use your own and set DB_* in server/.env)
docker compose up -d db
# 2. Configure + install
cp server/.env.example server/.env # edit DB_*, JWT_SECRET, ADMIN_USERNAME/PASSWORD
npm run install-server
# 3. Run the API (creates tables, seeds defaults + first admin on boot)
npm run server # http://localhost:3000 (API at /api/v1)
GET /api/health → { "status": "ok" } confirms it's up.
Deploy (Docker Compose)
cp .env.example .env # fill in DB creds, JWT_SECRET, admin, SMTP
docker compose up -d --build # app on 0.0.0.0:3000, MariaDB on the internal network
Point Pangolin at the app container on port 3000. The auth cookie auto-detects HTTPS, so
the admin panel works both via the LAN IP (HTTP) and through the proxy (HTTPS). The full app
image build requires client/ (frontend phase); until then the server runs API-only.
Key endpoints
| Group | Base | Auth |
|---|---|---|
| Auth | /api/v1/auth (login, logout, me) |
cookie |
| Public | /api/v1/public (settings, status, posts/:category, wiki, contact) |
none |
| Admin | /api/v1/admin (dashboard, site-mode, posts, wiki, settings, activity, users) |
cookie (admin) |
See BACKEND_DESIGN.md §4 for the complete contract.
Security notes
JWT in an httpOnly cookie · bcrypt hashing · login rate limiting · admin routes noindex ·
first admin seeded from env (no hardcoded credentials) · .env is git-ignored. SMTP is
optional — the contact form falls back to a mailto: link when SMTP is not configured.
Logging
Every log line goes to both the console and a log file, timestamped and leveled
(error / warn / info / debug):
2026-06-26T18:55:01.123Z INFO [server] listening on http://0.0.0.0:3000 ...
2026-06-26T18:55:09.880Z INFO [http] 192.168.1.40 admin POST /api/v1/auth/login 200 12 ms - 48 bytes
2026-06-26T18:55:14.402Z WARN [auth] login failed {"username":"root","ip":"192.168.1.40"}
2026-06-26T18:55:20.110Z ERROR [error] GET /api/v1/public/wiki -> 500 ... {"stack":"..."}
What's captured: startup config banner, schema/seed steps, HTTP access logs (real client
IP via trust proxy, the authenticated admin, method/URL/status/time/size), login
success/failure, rate-limit hits, site-mode changes, all errors with stack traces, and
graceful shutdown. Passwords and request bodies are never logged.
| Env | Default | Meaning |
|---|---|---|
LOG_LEVEL |
info |
console verbosity |
FILE_LOG_LEVEL |
debug |
file verbosity (keeps a full record) |
LOG_TO_FILE |
true |
set false for console-only |
LOG_DIR |
<server>/logs (/app/logs in Docker) |
log directory |
LOG_FILE |
app.log |
log file name |
In Docker the log file is bind-mounted to ./logs/app.log on the host; docker compose logs -f app
also shows the console stream.