# Architecture
A mental model of the DorkNet codebase for new contributors. Read this
before you touch code; it explains layout, request flow, the design
patterns that look weird in isolation, and where to start.
For deployment, see [`deploy.md`](deploy.md). For wire-protocol
reverse-engineering deliverables, see the `recroom-2020-client-*`
files in this folder.
---
## Projects in the solution
```
DorkNet.sln
├── DorkNet.Server ASP.NET Core 9 backend — the bulk of the code
├── DorkNet.Models Shared DTO + serialization types
└── DorkNet.ClientMod MelonLoader 6 IL2CPP mod — the client patcher
```
| Project | Responsibility | Target |
|---|---|---|
| `DorkNet.Server` | REST API, SignalR hub, admin SPA host, static-site host, image CDN, matchmaking, persistence | .NET 9 |
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 |
| `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 |
`ClientMod` applies the client-side patches needed to point the 2020
watch at a DorkNet server: URI rewrite (`.rec.net` → configured apex),
Photon AppId override, and TLS trust bypass. It loads under
MelonLoader 0.6.x and reads its settings from a JSON config under
`MelonLoader/UserData/`.
---
## Server folder layout
```
DorkNet.Server/
├── Auth/ JWT config, IP-ban + player-ban middleware, AdminOnly attribute
├── Compat/ Version detection middleware, client-version registry, HttpContext extensions
├── Controllers/ REST endpoints (see below)
├── Data/ DbContext, Entities/, EF migrations, LegacyUpgrades, seed JSON
├── Hubs/ SignalR NotifyHub (real-time presence on notify.{apex}/hub/v1)
├── Protos/ Protobuf schemas — trimmed `dorknet_room_data.proto` + full decompiled `recroom_2020.proto`
├── Services/ ~27 business-logic services (PlayerService, RoomService, etc.)
├── Versions/ Per-version plugins; Late2020VersionPlugin marks the 2020.12.18 build
├── admin-ui/ Vite + React SPA source — builds to wwwroot/admin/
├── site/ Public website Vite SPA source — builds to wwwroot/site/
├── wwwroot/ Static file root, with per-host subdirs (admin/, site/, feed/)
└── Program.cs Host + DI + middleware pipeline (split candidates noted at the bottom)
```
### Controllers/ groupings
20 top-level groups, each responsible for one product surface the
watch (game client) talks to. The grouping mirrors the watch's URL
layout, which is why some folders are large — splitting them would
obscure the mirror relationship.
| Folder | Surface |
|---|---|
| `API/` | Main REST API split by subsystem (Avatar, Store, Rooms, Messages, Inventions, Leaderboard, BugReporting, Moderation, …) |
| `Accounts/` | Account creation, login, profile updates |
| `Admin/` | Admin SPA backend, gated by `AdminOnlyAttribute` |
| `Auth/` | Photon custom-auth (`/photon/customauth`), version-check `/api/versioncheck/v4` |
| `Cdn/` | Image transforms, room blob serving |
| `Clubs/` | `clubs.{apex}/*` — clubs surface |
| `Commerce/` | `commerce.{apex}/*` — monetization catalog |
| `Discovery/` | `discovery.{apex}/*` — room discovery + search |
| `Econ/` | `econ.{apex}/*` — currency + inventory |
| `Geo/` | `geo.{apex}/*` — region lookup |
| `Health/` | `/healthz` probe |
| `Match/` | Matchmaking + `/goto/*` flow |
| `Notify/` | REST-side companion to the SignalR hub |
| `Ns/` | `ns.{apex}/*` — service-URL registry the watch queries on startup |
| `PlayerSettings/`, `Rooms/`, `Site/`, `Storage/`, `Strings/` | Smaller surfaces with self-explanatory scope |
---
## Request lifecycle
Middleware ordering matters in ASP.NET Core; the pipeline order in
`Program.cs` is below. Numbers are the order, not line numbers.
1. **Logging** — Serilog enriches every request with structured fields.
2. **HTTPS redirect** — Only when Kestrel has an HTTPS endpoint (direct
mode); a no-op behind nginx/cloudflared.
3. **WebSocket config** — 30-second keep-alive pings to keep
cloudflared from killing idle tunnels.
4. **Per-host static branches** (`MountStaticHost`) — `admin.{apex}`,
apex, `feed.{apex}` each serve their own SPA from `wwwroot/
`.
Mounted with `UseWhen` (not `MapWhen`) so API calls under those
subdomains still reach the controllers.
5. **IP-ban check** — earliest filter; rejects banned IPs before any
work runs.
6. **Version detection** (`VersionDetectionMiddleware`) — reads
`X-DorkNet-Version`, validates against the version registry,
returns 426 on mismatch. Apex + admin/site/feed subdomains skip
the gate (humans on browsers don't carry a client version).
7. **JWT authentication** — `UseAuthentication`.
8. **Authorization** — role/policy checks.
9. **Player-ban check** (`BanCheckMiddleware`) — after auth so it can
see `ctx.User`.
10. **MapControllers** — route to controller actions.
11. **SignalR hub** — `NotifyHub` on `notify.{apex}/hub/v1`, WebSocket-only,
`RequireHost` filter.
---
## Multi-tenancy + the domain config
DorkNet is built to run many concurrent players. Every feature assumes
multi-user semantics, not single-player sandbox. There's no global
"current player"; everything threads `playerId` through the request.
The deployment apex (default `localhost`, overridable via
`Domain:Apex` config or `DORKNET_DOMAIN` env var) is read once at
startup into the `DomainConfig` singleton. Inject it anywhere code
needs to build a subdomain URL — never hardcode `localhost`.
```csharp
public sealed class Foo(DomainConfig domain) {
public string Url() => $"https://cdn.{domain.Apex}/...";
}
```
Per-controller `[Host("api.rec.net", "api.localhost")]` filters are
**gone**; allowed hosts are derived from `{apex}` + `*.{apex}` and
enforced by `HostFilteringMiddleware`. Subdomain-discriminating
handlers branch on `Request.Host.Host.Split('.')[0]`.
---
## Version plugins
`Versions/` holds an `IVersionPlugin` per client generation we support.
Today there's one (`Late2020VersionPlugin` for the 2020.12.18 build).
The pattern is a marker registry:
- `VersionKeys` — the wire-format identifiers a plugin claims.
- `Generation` — human-readable name controllers branch on via
`ctx.GetClientVersion().Generation`.
- `RegisterStrategies(IServiceCollection)` — for plugins that ship
generation-specific implementations (today empty; will grow as
wire shapes diverge across builds).
When a new client build is wire-compatible with an existing
generation, just add its key to `VersionKeys`. When it diverges, add
a new plugin and register strategy bindings keyed by generation.
---
## Reverse-engineering provenance
DorkNet is a clean-room reimplementation of the 2020.12.18 Rec Room
backend protocol. ~46 files carry `Cpp2IL` provenance comments tying
specific code to decompiled client internals. Example:
```csharp
// Wire shape per Cpp2IL_ISIL/.../PLILLKHMNDA.txt deserializer:
// gap-skipping enum, not 0..N
public const int Member = 10, Moderator = 20, CoOwner = 30, Creator = 100;
```
The decompiled dumps themselves are not in this repo (`dist/RecRoom-2020.12.18-isil/`
locally, gitignored). The comments are the bridge — if you're writing
or fixing wire-shape code, always check the decompiled source first
rather than guessing from symptoms.
The `docs/recroom-2020-client-*` files are an exhaustive catalog of
client-side endpoints, DTOs, and request expectations extracted from
the same decompile. They're the ground truth when a controller needs
a new endpoint.
---
## Database story
Two providers, switched by `Database:Provider` config:
- **SQLite** — default for local dev. File at `bin//data/dorknet.db`.
- **Postgres** — production. Connection string from `ConnectionStrings:Default`.
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. EF
migrations under `Data/Migrations/` apply via `Database.Migrate()` on
boot. Data transforms that can't be expressed as schema migrations
(seed renames, computed backfills) go in `Data/LegacyUpgrades.cs` as
idempotent methods registered in `RunAsync()`. See
[`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the
discipline: one release = one migration; everything else is
`LegacyUpgrades`.
---
## Where to start
If you've never touched this codebase, the most approachable path:
1. Read a small **Service** (`ConfigService`, `DomainConfig`,
`OnlinePresenceService`) to learn the DI pattern.
2. Read a small **Controller** (`Health/HealthController`,
`Strings/StringsController`) to see how services are injected and
responses are shaped.
3. Trace one **end-to-end request** through `Rooms/` or `Players/` —
middleware → controller → service → DbContext → response. The
middleware pipeline (above) is the lens.
4. Look at one **`Cpp2IL`-cited line** in a controller to see how
wire shapes are pinned to decompiled provenance.
When in doubt: open `DorkNet.Server/Program.cs` and the
`Versions/Late2020/Late2020VersionPlugin.cs` summary doc-comment.
Between them they explain 80% of the cross-cutting decisions.
---
## Acknowledged sharp edges
These look messy. They're messy on purpose — the alternative is
worse.
- **Some controllers are large.** `AdminController.cs`, `RoomsController.cs`,
`GoToController.cs` are big because they mirror watch surfaces that
are themselves sprawling. Splitting them by endpoint group would
obscure the surface-mirror relationship that makes the code
navigable when paired with the watch's decompiled call sites.
- **Cpp2IL comments reference paths outside the repo.** That's
intentional — the decompiled source is provenance only, not
shippable. Comments give a future contributor a breadcrumb to
re-derive the wire shape if the decompile changes.
- **`Program.cs` is long.** Most of it is one-time schema patching
(`RunPatchAsync` calls for the Postgres `EnsureCreated` path) that's
pending consolidation per [`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md).
Once that lands, the remaining ~500 lines of middleware + host
setup will be split into `Startup/*` extensions.
- **Two-provider EF.** SQLite and Postgres each have their own
EnsureCreated/Migrate dance because some Postgres-only column types
drift the model snapshot. The split is annoying but lets one
codebase serve both local dev (zero-config SQLite) and production
(managed Postgres).
- **Hardcoded `rec.net` URLs in `RoomService` + `HtrAssetMirrorService`.**
These pull canonical assets (room thumbnails, `.htr` asset data)
from the official Rec Room CDN at seed time. The alternative is
shipping the assets in the repo, which would put DorkNet on
legally weaker footing.