diff --git a/README.md b/README.md
index c0c031b..67f5796 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/Runic
## Contents
+- [Architecture](#architecture)
- [Tech stack](#tech-stack)
- [Project structure](#project-structure)
- [Prerequisites](#prerequisites)
@@ -44,6 +45,102 @@ The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/Runic
---
+## Architecture
+
+How the pieces fit together — the React SPA and native app talk to one Express backend
+(`router → controller → model → db`), which persists to MariaDB and bridges to the live
+game world only through the **uo-link** sidecar. The shard itself is never internet-facing.
+
+```mermaid
+flowchart TB
+ %% ---------- Clients ----------
+ subgraph clients["Clients"]
+ browser["Browser
React + Vite SPA
(public · wiki · admin)"]
+ mobile["Native mobile app
(bearer tokens)"]
+ end
+
+ idp["SSO providers
Google · Discord · custom OIDC"]
+ discord["Discord"]
+
+ %% ---------- Website (one repo) ----------
+ subgraph website["website/ — Node app (one repo)"]
+ direction TB
+
+ subgraph backend["server/ — Express backend"]
+ direction TB
+ mw["Middleware
helmet · siteMode · noindex
rateLimit · loginProtection · botScore · validate"]
+ router["Router /api/v1
auth (web · mobile · sso) · public · admin"]
+ ctrl["Controllers"]
+ auth["Session layer (auth/)
sessionService · JWT/cookie · bearer · SSO+PKCE"]
+ model["Models (.model + .db)
raw parameterized SQL — no ORM"]
+ sse["SSE fan-out
public stream (allowlist) · admin stream (sensitive)"]
+
+ subgraph shardutil["Shard integration (utils/)"]
+ ingest["shardIngest.js
WS ingest dispatcher"]
+ restcli["uoLinkClient.js
REST client (never throws)"]
+ end
+
+ secret["secretBox.js
AES-256-GCM secrets at rest"]
+ end
+
+ bot["bot/
Discord bot"]
+ end
+
+ db[("MariaDB
users · posts · wiki · settings · activity
mobileSessions · authProviders · userIdentities
uoLinkConfig · shard_online/economy/houses/events")]
+
+ %% ---------- Shard side ----------
+ subgraph shardside["Game shard (never internet-facing)"]
+ direction TB
+ sidecar["uo-link sidecar
(Rust) — the only bridge exposed"]
+ servuo["ServUO shard
(C# plugin)"]
+ end
+
+ %% ---------- Edges ----------
+ browser <-->|"same-origin JSON + SSE (cookie)"| mw
+ mobile -->|"REST (bearer access/refresh)"| mw
+ browser -.->|"OAuth redirect + PKCE"| idp
+ auth -.->|"token exchange"| idp
+
+ mw --> router --> ctrl
+ ctrl --> auth
+ ctrl --> model
+ ctrl --> restcli
+ ctrl --> sse
+ auth --> model
+ model <--> db
+ auth -. reads/writes secrets .-> secret
+ restcli -. reads config/token .-> secret
+ ingest --> model
+ ingest --> sse
+ sse -->|"live events"| browser
+ bot -->|"messages"| discord
+ bot <--> db
+
+ restcli -->|"REST: /char /roster /economy /history · /link/confirm · /towncrier"| sidecar
+ sidecar -->|"WebSocket live event feed (bearer + X-UOLink-Version)"| ingest
+ servuo -->|"loopback TCP 127.0.0.1:7788
newline-delimited JSON (shard dials out)"| sidecar
+
+ %% ---------- Styling ----------
+ classDef ext fill:#2d2233,stroke:#7a5c94,color:#e8dff0;
+ classDef store fill:#1f2d2a,stroke:#4c8c7d,color:#dff0ea;
+ classDef bridge fill:#2d2620,stroke:#94764c,color:#f0e6d8;
+ class idp,discord ext;
+ class db store;
+ class sidecar,servuo bridge;
+```
+
+- **One backend, layered.** Every request flows `middleware → router → controller → model → db`.
+ Web browsers authenticate with an httpOnly JWT cookie; the native app uses short-lived bearer
+ access tokens plus rotated refresh tokens; SSO (Google/Discord/OIDC) is link-only and PKCE-guarded.
+ All three surfaces produce the *same* session via the session layer.
+- **The shard is never reachable.** The ServUO shard *dials out* over loopback TCP to the uo-link
+ sidecar; only the sidecar is exposed, and only the backend talks to it. The REST client
+ (`uoLinkClient.js`) never throws, so the site degrades gracefully when the shard is down.
+- **Sensitive events stay private.** Ingested game events fan out to browsers over two SSE channels —
+ a public allowlist stream and an admin-only stream that adds staff audit / cheat / login events.
+
+---
+
## Tech stack
| Layer | Tech |