docs(website): add architecture diagram
Add docs/website/ARCHITECTURE.md (canonical copy of the website architecture Mermaid diagram, with fuller notes) and embed the same diagram in the website-README mirror. Mirrors the diagram added to the website repo README. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
114
website/ARCHITECTURE.md
Normal file
114
website/ARCHITECTURE.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Website — Architecture
|
||||||
|
|
||||||
|
How the pieces of `RunicGateway/website` fit together. The React SPA and the native mobile 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 ServUO shard itself is
|
||||||
|
never internet-facing — it dials out to the sidecar over loopback, and only the sidecar is exposed.
|
||||||
|
|
||||||
|
This is the canonical copy of the diagram; the same diagram is embedded in the website's
|
||||||
|
[`README.md`](https://gitea.whitlocktech.com/RunicGateway/website/src/branch/main/README.md#architecture).
|
||||||
|
See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) for the full API / schema / security contract, and
|
||||||
|
[`docs/link/`](../link/) for the wire protocol between the sidecar and the shard.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
%% ---------- Clients ----------
|
||||||
|
subgraph clients["Clients"]
|
||||||
|
browser["Browser<br/>React + Vite SPA<br/>(public · wiki · admin)"]
|
||||||
|
mobile["Native mobile app<br/>(bearer tokens)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
idp["SSO providers<br/>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<br/>helmet · siteMode · noindex<br/>rateLimit · loginProtection · botScore · validate"]
|
||||||
|
router["Router /api/v1<br/>auth (web · mobile · sso) · public · admin"]
|
||||||
|
ctrl["Controllers"]
|
||||||
|
auth["Session layer (auth/)<br/>sessionService · JWT/cookie · bearer · SSO+PKCE"]
|
||||||
|
model["Models (.model + .db)<br/>raw parameterized SQL — no ORM"]
|
||||||
|
sse["SSE fan-out<br/>public stream (allowlist) · admin stream (sensitive)"]
|
||||||
|
|
||||||
|
subgraph shardutil["Shard integration (utils/)"]
|
||||||
|
ingest["shardIngest.js<br/>WS ingest dispatcher"]
|
||||||
|
restcli["uoLinkClient.js<br/>REST client (never throws)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
secret["secretBox.js<br/>AES-256-GCM secrets at rest"]
|
||||||
|
end
|
||||||
|
|
||||||
|
bot["bot/<br/>Discord bot"]
|
||||||
|
end
|
||||||
|
|
||||||
|
db[("MariaDB<br/>users · posts · wiki · settings · activity<br/>mobileSessions · authProviders · userIdentities<br/>uoLinkConfig · shard_online/economy/houses/events")]
|
||||||
|
|
||||||
|
%% ---------- Shard side ----------
|
||||||
|
subgraph shardside["Game shard (never internet-facing)"]
|
||||||
|
direction TB
|
||||||
|
sidecar["uo-link sidecar<br/>(Rust) — the only bridge exposed"]
|
||||||
|
servuo["ServUO shard<br/>(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<br/>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;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes on the diagram
|
||||||
|
|
||||||
|
- **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, hashed, revocable refresh tokens; SSO (Google/Discord/OIDC) is
|
||||||
|
link-only and PKCE-guarded. All three surfaces resolve to the *same* session model via the session
|
||||||
|
layer, and admin routes are re-validated against the DB on every request.
|
||||||
|
- **The shard is never reachable.** The ServUO shard *dials out* over loopback TCP `127.0.0.1:7788`
|
||||||
|
(newline-delimited JSON) to the uo-link sidecar; only the sidecar is exposed, and only the backend
|
||||||
|
talks to it. Every backend→sidecar call carries `Authorization: Bearer <token>` and an
|
||||||
|
`X-UOLink-Version` header (a protocol mismatch fails fast with `409`). The REST client
|
||||||
|
(`uoLinkClient.js`) never throws — every call returns `{ ok, data, status }` — so the site degrades
|
||||||
|
gracefully when the shard is down.
|
||||||
|
- **Two ways in from the sidecar.** Live game events arrive over an outbound **WebSocket** and are
|
||||||
|
routed by the `shardIngest.js` dispatcher (state-changing kinds update `shard_*` tables, notable
|
||||||
|
kinds append to `shard_events`, high-frequency kinds only update state). Point-in-time reads and
|
||||||
|
commands go over **REST** through `uoLinkClient.js`.
|
||||||
|
- **Sensitive events stay private.** Ingested events fan out to browsers over two **SSE** channels —
|
||||||
|
a public allowlist stream and an admin-only stream that additionally carries staff audit, cheat
|
||||||
|
detection, and login-attempt events. The allowlist split is a security boundary; sensitive kinds
|
||||||
|
can never leak onto the public channel.
|
||||||
|
- **Secrets at rest.** OAuth client secrets, the uo-link token, and the Gmail refresh token are
|
||||||
|
AES-256-GCM encrypted via `secretBox.js` (keyed by `SECRET_ENC_KEY`). The uo-link token is
|
||||||
|
write-only in the API — never returned to any client.
|
||||||
@@ -18,6 +18,7 @@ The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, sc
|
|||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
|
- [Architecture](#architecture)
|
||||||
- [Tech stack](#tech-stack)
|
- [Tech stack](#tech-stack)
|
||||||
- [Project structure](#project-structure)
|
- [Project structure](#project-structure)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
@@ -37,6 +38,103 @@ The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, sc
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
See [ARCHITECTURE.md](ARCHITECTURE.md) for the fuller write-up.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
%% ---------- Clients ----------
|
||||||
|
subgraph clients["Clients"]
|
||||||
|
browser["Browser<br/>React + Vite SPA<br/>(public · wiki · admin)"]
|
||||||
|
mobile["Native mobile app<br/>(bearer tokens)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
idp["SSO providers<br/>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<br/>helmet · siteMode · noindex<br/>rateLimit · loginProtection · botScore · validate"]
|
||||||
|
router["Router /api/v1<br/>auth (web · mobile · sso) · public · admin"]
|
||||||
|
ctrl["Controllers"]
|
||||||
|
auth["Session layer (auth/)<br/>sessionService · JWT/cookie · bearer · SSO+PKCE"]
|
||||||
|
model["Models (.model + .db)<br/>raw parameterized SQL — no ORM"]
|
||||||
|
sse["SSE fan-out<br/>public stream (allowlist) · admin stream (sensitive)"]
|
||||||
|
|
||||||
|
subgraph shardutil["Shard integration (utils/)"]
|
||||||
|
ingest["shardIngest.js<br/>WS ingest dispatcher"]
|
||||||
|
restcli["uoLinkClient.js<br/>REST client (never throws)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
secret["secretBox.js<br/>AES-256-GCM secrets at rest"]
|
||||||
|
end
|
||||||
|
|
||||||
|
bot["bot/<br/>Discord bot"]
|
||||||
|
end
|
||||||
|
|
||||||
|
db[("MariaDB<br/>users · posts · wiki · settings · activity<br/>mobileSessions · authProviders · userIdentities<br/>uoLinkConfig · shard_online/economy/houses/events")]
|
||||||
|
|
||||||
|
%% ---------- Shard side ----------
|
||||||
|
subgraph shardside["Game shard (never internet-facing)"]
|
||||||
|
direction TB
|
||||||
|
sidecar["uo-link sidecar<br/>(Rust) — the only bridge exposed"]
|
||||||
|
servuo["ServUO shard<br/>(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<br/>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
|
## Tech stack
|
||||||
|
|
||||||
| Layer | Tech |
|
| Layer | Tech |
|
||||||
|
|||||||
Reference in New Issue
Block a user