Architecture
How Invarn is put together, and why.
Layers
Entry points (bot/ for Discord, handlers/ for HTTP, jobs/ for River) call service/, one
function per action, whichever way it's asked: a slash command, a button and a dashboard request run
the same code. Services use store/ (queries generated by sqlc) and discord/, the only package
that talks to Discord's API.
Previews
Every change to Discord goes through discord.Actions. It has two implementations: the live client,
and a recorder that writes one readable line per call instead. A preview runs the same service
function inside a database transaction that's rolled back, with the recorder; the lines are what the
dashboard shows before you apply.
Scaling
Processes don't talk to each other; they meet in Postgres. Each owns some gateway shards, and each server's events go to one worker in order, so in-memory state needs no locks. Discord's data (roles, channels, expressions) lives in disgo's cache, filled from the gateway or over REST.
The journal
Every change is written once, with its before-state, after Discord's audit log says who made it (changes wait up to five seconds for that). Invarn's own changes are attributed to whoever asked, from the audit reason. Undo reverses entries in a safe order, recreating what was deleted and remapping IDs for the entries that follow.
Packs
Each server converges on its packs in a job: the desired items in priority order fill the slots left by expressions in no pack. The database changes before Discord does, so the events that come back match it and nothing loops. A per-server counter makes sure a change made while a job runs is never missed.