Skip to content

Architecture

┌────────────┐
nit (developer) │ │ PostgreSQL
──────────────▶│ nitd │◀──────────────
│ │
└─────┬──────┘
│ queue
┌─────▼──────┐
│ nit-worker │───────────────▶ the forge
└────────────┘
BinaryRoleRuns where
nitDeveloper CLIA developer’s machine
nitdControl plane: API, authorization, sync points, queue, blobs, auditOne or more replicas
nit-workerClones, applies, filters, pushesAnywhere with git, disk and forge access
nitctlOperator CLI: policy, tokens, migrations, tasks, auditAn operator’s machine or a server

Plus an optional web console, which is a client of the same read-only operations API that nitctl uses.

nitd performs no git operation. Everything that clones, applies or pushes runs in a worker.

A repository can be enormous and a forge can be slow. If the API did that work, one large clone would hold an HTTP request open and consume a connection for minutes. Separating them also means they scale on different axes: nitd scales with request rate, workers scale with disk and network.

Push and pull share the clone handling, the patch pipeline and the sync point bookkeeping — ninety percent of the code. Two binaries would double the deployment surface for no gain.

Scaling is per queue instead:

Terminal window
nit-worker # takes both kinds
nit-worker -queues=pull # a machine dedicated to read traffic
nit-worker -concurrency=4 # four runners, four concurrent clones

Rules, groups and repositories live in a bundle — YAML in its own git repository, reviewed through pull requests. Not in the database.

That is what gives authorization rules history, blame and rollback, and what makes nitctl policy validate runnable in CI. A database row has none of those properties, and “who granted this access, and when?” is the first question asked after an incident.

The consequence, which is worth accepting knowingly: the console cannot edit rules. Granting access is a pull request, not a click. That is slower on purpose.

TableHolds
tasksThe queue, with leases and fencing tokens
sync_points(workspace, repository, branch) → upstream commit
workspacesOne checkout on one machine
sessionsAuthentication tokens, as hashes only
artifactsBlob metadata, with TTLs
audit_logAppend-only, enforced by the database
users, repositoriesMirrors of the bundle, so records have something to reference

The audit table carries a trigger that refuses UPDATE, DELETE and TRUNCATE with an error, so an application bug cannot rewrite history — and an operator who tries is told rather than quietly ignored. (TRUNCATE is the exception on MySQL and MariaDB, which fire no trigger for it; there the line is held by withholding the DROP privilege.)

Retention is an operator action, never the application’s: nitctl audit prune is the only way to remove records, it goes to the database rather than the API, and it records itself in the trail it is emptying. See Retention.

Content-addressed by the SHA-256 of their compressed bytes. That gives deduplication, resumable transfer, and integrity checking the client can perform itself.

Filesystem-backed today, behind an interface so object storage is a swap rather than a rewrite.

Tokens are issued by an operator (nitctl token create) and only their SHA-256 is stored. A leaked database dump yields nothing usable, and there is no recovery path for a lost token — it is reissued, not retrieved.

Identity never comes from the patch. The author field of a git commit is free text; a system that trusted it would let anyone attribute a change to a colleague. The commit that lands upstream is authored from the authenticated session.

Authentication failures are distinguished — expired, revoked, disabled, not in the policy bundle — because the right action differs for each, and answering “unauthorized” to all of them is an operational problem rather than a security feature.

nitd is stateless apart from the database. Run as many replicas as you like behind a load balancer. They must share NIT_SYNC_KEY, or a token minted by one and rejected by another would make the deployment fail at random.

Workers scale horizontally. Pushes still serialize per branch — that is the queue’s job — so more workers buy throughput across branches and repositories, never within one.

A worker that dies releases its branch when its lease lapses. A reaper returns abandoned tasks to the queue, and the fencing token stops the dead worker’s process from completing a task another worker now owns.

A bundle that does not compile is not applied. The last good one stays in force and the failure is logged loudly — failing open would grant access nobody authorized, and failing closed would take an outage on every typo.

Migrations are never applied on boot. A schema change is a deployment step an operator takes with nitctl migrate; a server that migrated on start-up would happily run half-rolled-out DDL from several replicas at once.

nit ships single-tenant. A tenant_id is carried through the schema and the domain types from the start, always default, so the capability can be added without migrating everything — threading a tenant through a schema after the fact is one of the most expensive migrations there is.