Deletes Server/store (SQLiteStore, MemStore, the composed Store interface) and collapses to a single sqlc-backed db package, executing the prior audit's P4 "single data layer" direction (finding #6). SQLiteStore was a pure pass-through to *db.DB, so consumers now depend on narrow interfaces that *db.DB satisfies directly: - service.Store (service/datastore.go, renamed from store/store.go) - ws.EventStore (ws/eventstore.go) - plugin.PluginStore (plugin/pluginstore.go) The event- and plugin-KV methods that lived in the store's SQLite implementation move into the db package (db/event_queries.go, db/plugin_queries.go), keeping their raw-SQL form. Tests: the MemStore-based unit tests now run against a real in-memory SQLite db opened per-test with migrations applied, via package-local seed helpers. Fault-injection tests embed a real *db.DB and override the single method under test, preserving error-path coverage. Full server suite and sqlc-verify are green. Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3 updated; architecture server.md / data-model.md diagrams and prose updated to the api -> service -> db layering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
5.8 KiB
Data Model
Verified against: commit ddc49f0, 2026-07-19
The canonical schema is the ordered migration set Server/migrations/001–015
(embedded via go:embed, applied by the custom runner in Server/db/migrate.go,
tracked in the schema_versions table). SQLite is the only supported engine —
Server/main.go rejects any other database.type at startup.
D5 — Entity-relationship overview
All 23 application tables, grouped by domain. Junction/leaf detail columns are
elided; the goal is the relationship graph, not full DDL (see docs/schema.md
for DDL — note it is currently 6 migrations behind, see
audit-2026-07-19.md §2).
erDiagram
%% ── Identity & access ──
roles ||--o{ users : "role_id"
users ||--o{ sessions : "user_id"
roles ||--o{ channel_overrides : "role_id"
channels ||--o{ channel_overrides : "channel_id"
users ||--o{ user_blocks : "blocker_id / blocked_id"
users ||--o{ invites : "created_by / redeemed_by"
%% ── Messaging ──
channels ||--o{ messages : "channel_id"
users ||--o{ messages : "user_id"
messages ||--o{ messages : "reply_to"
messages ||--o{ attachments : "message_id"
messages ||--o{ reactions : "message_id"
users ||--o{ reactions : "user_id"
users ||--o{ read_states : "user_id"
channels ||--o{ read_states : "channel_id"
%% ── Direct messages (channels with type='dm') ──
channels ||--o{ dm_participants : "channel_id"
users ||--o{ dm_participants : "user_id"
channels ||--o{ dm_open_state : "channel_id"
users ||--o{ dm_open_state : "user_id"
%% ── Voice ──
users ||--o| voice_states : "user_id (PK)"
channels ||--o{ voice_states : "channel_id"
%% ── Plugins ──
plugins ||--o{ plugin_kv : "plugin_id"
%% ── Standalone (no FK edges) ──
events
settings
audit_log
login_attempts
rate_lockouts
emoji
sounds
users {
int id PK
int role_id FK
string username
string password_hash
bool banned
}
roles {
int id PK
string name
int permissions "31-bit bitfield"
int position
}
channels {
int id PK
string name
string type "text | voice | dm (trigger-enforced)"
}
messages {
int id PK
int channel_id FK
int user_id FK
int reply_to FK
string content
}
attachments {
string id PK "UUID"
int message_id FK
int uploader_id "added by 010"
}
events {
int seq PK "AUTOINCREMENT, hub seq seeded from MAX(seq)"
string type
string payload
}
Domain notes
| Domain | Tables | Notes |
|---|---|---|
| Identity & access | roles, users, sessions, channel_overrides, user_blocks, invites, login_attempts, rate_lockouts |
Sessions store only SHA-256 token hashes. Permissions are a bitfield on roles.permissions; channel overrides use Discord semantics (role &^ deny) | allow. rate_lockouts (011) persists rate-limiter lockouts across restarts. |
| Messaging | channels, messages, attachments, reactions, read_states, emoji |
channels.type is constrained to text | voice | announcement | dm by INSERT/UPDATE triggers (migration 013, extended by 016 to allow announcement). Announcement channels read like text but require MANAGE_MESSAGES to post. attachments.uploader_id (010) backs upload-ownership checks. |
| Direct messages | dm_participants, dm_open_state |
DMs are channels rows with type='dm'; these tables track membership and per-user open/closed UI state (009). |
| Voice | voice_states |
One row per user (user_id is the PK) — a user occupies at most one voice channel. |
| Real-time replay | events |
Cold tier of the 3-tier reconnect replay (websocket.md); written by the async EventPersister, pruned by retention. Hub seq counter is seeded from MAX(events.seq) at startup so seqs stay monotonic across restarts. |
| Plugins | plugins, plugin_kv |
015. plugin_kv is per-plugin namespaced KV via composite PK (plugin_id, key). |
| Ops | settings, audit_log, sounds |
settings is a generic KV read by admin and the WS hub (via db.GetSetting). Migration 003 rebuilds audit_log through a transient audit_log_v6 rename — only audit_log exists at runtime. sounds is dead schema — the soundboard feature was removed but the table remains. |
How the schema is accessed
The Server/db package is the single data layer. Its methods live in
*_queries.go and mostly delegate to the sqlc-generated Server/db/dbgen code
(D2), so sqlc is the type-checked query layer rather than dead generated code; a
documented remainder of raw queries stays hand-written where sqlc can't express
them (variable-length IN lists, FTS, multi-statement transactions,
PRAGMA/VACUUM) — see plans/sqlc-adoption.md.
Consumers depend on narrow interfaces that *db.DB satisfies rather than on the
concrete type: service.Store (the service layer), ws.EventStore (cold-tier
replay), and plugin.PluginStore (plugin registry). D3 removed the former
store package (a pass-through SQLiteStore plus a MemStore test double);
tests now run against a real in-memory SQLite db. The residual style issue is
that many Server/api handlers and the Server/admin package still take a raw
*db.DB directly instead of going through the service layer — the remaining
consolidation work (audit A-2026-07-06).
Source of truth: Server/migrations/*.sql (schema), Server/db/migrate.go
(runner), Server/db/*_queries.go (live queries), Server/service/datastore.go
(service interface), sqlc.yaml + Server/db/dbgen/ (generated query layer).