Files
OwnCord/docs/architecture/server.md
T
J3vb d383d8c7e8 feat(b3-0): boundary inventory — dbinventory tool, db-import-boundary rule, server-boundaries.md (#1448)
* docs(b3): Codex round 1 — keep the main-PR Docker term, checkout dev on schedule, profile numbers, hub built in api.NewRouter

P1: the Docker verify condition keeps ref_name/base_ref main and adds the
schedule term. P2: scheduled runs check out dev explicitly (the workflow file
comes from main). P2: the alpha profile's dimensions are defined in the plan,
not borrowed from load-baseline.yml (which has only users=100). P2: ws.NewHub
is called in api/router.go:106 with setters split across router.go and
main.go, so B3-4 follows B3-3, which moves construction into internal/app.
Also: plan-index row and roadmap slice line for B3.

* feat(b3-0): boundary inventory — dbinventory tool, db-import-boundary rule, server-boundaries.md

51 production files outside db/ and service/ import db (ws 17, admin 16,
api 12, auth 2, root 2, cmd/seed 1, plugin 1); 14 are type-only. Each has a
disposition (move 28 / adapter 17 / boundary 6) and, for moves, a target
family, held in invariants.DBImportAllow so the generated document and the
gate cannot drift. The rule fails any new importer without a row; the live
test fails any stale row. Hub lifecycle (setters, locks, defer stack) and the
auth before-graph are inventoried for B3-2/B3-3/B3-4. Closes the B3 entry
gate's third item.

* fix(b3-0): dbinventory exempts only top-level db/ and service/ (Codex P2)

Skipping by directory name let a nested api/service/ escape the inventory
while the rule would still catch it; the walker now exempts by root-relative
path, with a test over a synthetic tree.
2026-08-29 20:07:15 +02:00

6.2 KiB
Raw Blame History

Server Architecture

Verified against: commit 5630aa1, 2026-08-04

Single Go binary (github.com/J3vb/OwnCord/Server, Go 1.26). Pure-Go SQLite (modernc.org/sqlite, no CGO), chi router, github.com/coder/websocket, LiveKit for voice, Wazero for plugins (build-tag gated), optional OpenTelemetry (-tags otel). Roughly 42k LOC of production code and 71k LOC of tests.

D2 — Package map

Which of these packages may import db, and what every file above the domain layer does with it, is inventoried in server-boundaries.md (B3-0) and enforced by the db-import-boundary rule in Server/invariants/.

flowchart TB
    subgraph entry ["Process entry"]
        MAIN["main.go<br/>config, TLS, DB open+migrate,<br/>signal handling, maintenance loop"]
    end

    subgraph http ["HTTP layer"]
        API["api<br/>router, middleware, REST handlers,<br/>WAF, LiveKit proxy, uploads"]
        ADMIN["admin<br/>embedded SPA + admin REST,<br/>SSE log stream"]
    end

    subgraph realtime ["Real-time"]
        WS["ws<br/>Hub, pub/sub, replay,<br/>typed command dispatch, LiveKit, E2EE relay"]
    end

    subgraph domain ["Domain"]
        SVC["service<br/>Message/Channel/Permission/User/<br/>DM/Invite/Block/Moderation/Voice"]
        PERM["permissions<br/>bitfield + Checker"]
        PLUGIN["plugin<br/>wazero runtime, registry,<br/>manifest, host APIs"]
    end

    subgraph data ["Data"]
        DB[("db<br/>query methods (sqlc-backed),<br/>migration runner, models")]
        DBGEN["db/dbgen<br/>sqlc-generated queries"]
        MIG["migrations<br/>001028 embedded SQL"]
    end

    subgraph support ["Support"]
        AUTH["auth<br/>bcrypt, tokens, TOTP,<br/>rate limiting, TLS"]
        CFG["config<br/>koanf: defaults→YAML→env"]
        STORAGE["storage<br/>upload files on disk"]
        TEL["telemetry<br/>OTel (no-op default)"]
        UPD["updater<br/>minisign-verified self-update"]
    end

    MAIN --> CFG
    MAIN --> API
    MAIN --> DB
    API --> ADMIN
    API --> WS
    API --> SVC
    API --> AUTH
    API --> STORAGE
    API --> UPD
    API --> PLUGIN
    SVC --> DB
    SVC --> PERM
    DB --> DBGEN
    DB --> MIG
    WS --> SVC
    WS --> PLUGIN

    %% Residual layering seam (dashed): raw *db.DB used above the service layer
    API -.->|"handlers take *db.DB directly"| DB
    ADMIN -.->|"raw *db.DB"| DB

What this shows. The layering is api → service → db (D3 removed the former store seam). The service layer depends on a narrow service.Store interface that *db.DB satisfies; ws and plugin depend on their own small interfaces (ws.EventStore, plugin.PluginStore) the same way. The db package's query methods delegate to the sqlc-generated db/dbgen code (D2), so sqlc is now the type-checked query layer rather than dead generated code. Two dashed edges mark the residual seam: many REST handlers still receive a raw *db.DB alongside svc, and the admin package operates on *db.DB almost exclusively — consolidating those behind the service layer is the remaining work (audit A-2026-07-06 — resolved for the store seam itself; the residual consolidation is its backlog item 12). See data-model.md.

api.NewRouter (Server/api/router.go) is the composition root: it constructs the rate limiter, TOTP key, storage, service.New, the ws.Hub, the LiveKit client/subprocess, the updater, the admin handler, and the plugin admin handler; spawns background goroutines; and mounts all routes. main.go performs only process-level wiring (config, TLS, DB, event persistence, HTTP server, shutdown).

The plugin package is experimental and compiled out of release binaries; plugins.md records that boundary — what exists, what carries no promise, and which core concerns will never move behind it.

Source of truth: Server/main.go, Server/api/router.go, package import graph (go list -deps), Server/service/datastore.go, sqlc.yaml.

D3 — REST request lifecycle

sequenceDiagram
    autonumber
    participant C as Client
    participant MW as Global middleware<br/>(api/router.go)
    participant RT as Route mount<br/>(Mount*Routes)
    participant H as Handler
    participant S as service.*
    participant ST as service.Store<br/>(*db.DB)
    participant DB as SQLite

    C->>MW: HTTPS request
    Note over MW: RequestID → Recoverer → requestLogger<br/>→ telemetry → SecurityHeadersWithTLS<br/>→ MaxBodySizeUnless(uploads exempt)<br/>→ optional Coraza WAF
    MW->>RT: routed by chi
    Note over RT: AuthMiddleware(database)<br/>+ per-route rate limits<br/>+ RequirePermission(...) where mounted
    RT->>H: authenticated request
    H->>S: domain call (svc.Messages, svc.Permissions, …)
    S->>ST: narrow Store interface method
    ST->>DB: SQL (db.DB query method → sqlc dbgen)
    DB-->>C: JSON response (errorResponse envelope on failure)

    rect rgba(200,120,120,0.15)
        Note over H,DB: Deviation — auth routes: MountAuthRoutes(r, database, …)<br/>bypasses the service layer and queries *db.DB directly.<br/>Admin REST (Server/admin) does the same behind<br/>AdminIPRestrict + RequireAdminAuth.
    end

What this shows. The global chain is assembled in NewRouter; note that chi's middleware.RealIP is deliberately omitted — client IP is resolved via clientIPWithProxies against configured trusted proxies instead, so spoofed X-Real-IP/X-Forwarded-For headers are not trusted by default. Authentication is bearer-token (SHA-256-hashed opaque tokens); authorization is enforced at two deliberate scopes (D13): RequirePermission middleware gates the two channel-less routes on server-wide role permissions via permissions.HasServerPerm (channel overrides deliberately not consulted — a per-channel allow must never open a server-wide gate), while anything channel-scoped is checked in the service layer through svc.Permissions / permissions.Checker, which resolves overrides and fails closed if they cannot be fetched. The shaded region marks the two documented bypass paths of the domain layer.

Source of truth: Server/api/router.go, Server/api/middleware.go, Server/api/auth_handler.go, Server/admin/middleware.go, Server/permissions/, Server/service/.