Day-to-day operations
Tokens
Section titled “Tokens”nitctl token create -user alice -label laptop -ttl 720hnitctl token list -user alicenitctl token revoke -id <session-id>Only the SHA-256 of a token is stored. It is shown once and there is no recovery path: a lost token is reissued, not retrieved.
A token can only be issued to someone the bundle declares — a typo produces an error rather than a credential for an account that authorizes nothing.
Revocation is immediate. A second revoke keeps the first instant, because for an incident timeline when a credential was first cut off is the fact that matters.
Removing someone
Section titled “Removing someone”Take them out of the bundle. Authentication then fails with user_not_in_policy
and every grant is gone.
Their history stays: nit never deletes a user row, because a deleted row would take the audit trail with it. Revoke their tokens too, for tidiness.
Watching the queue
Section titled “Watching the queue”export NIT_SERVER=https://nit.example.comexport NIT_TOKEN=nit_…
nitctl statspolicy: sha256:f6040b6d6a8381dcrepositories: 12
queued: 3running: 1succeeded: 1847failed: 2
busy branches: 1denials (24h): 6Two numbers mean something is wrong right now: queued climbing, and branches busy staying high. Everything else is history.
Denials (24h) is a trend, not an alarm. A number that climbs is usually the policy fighting the team — some subtree’s ownership no longer matches who works on it — rather than anybody attacking anything.
nitctl tasks -state queuednitctl tasks -state failed -limit 20nitctl tasks -repository backend-api -kind pushTASK KIND STATE USER REPOSITORY@BRANCH DURATION NOTE9ccf9693-56d4-4bec-838b-079867bcf379 pull succeeded bob backend-api@main 38msbe59af45-9694-416e-ace2-da5cffc7f145 push running carol backend-api@feature/x 2m14s worker-256c8b3f7-a0a1-4971-b5ba-3f18389b08bb push failed dave data-platform@main 1.2s conflictNOTE carries the error code for a failure, the queue position for a wait, or
the worker holding the lease for a running task.
The audit trail
Section titled “The audit trail”nitctl audit -limit 20nitctl audit -user bob -since 24hnitctl audit -repository backend-api -since 168hnitctl audit -request <request-id> -jsonWHEN ACTOR ACTION REPOSITORY@BRANCH PATH RULE2026-07-31 00:35:29 bob push.applied backend-api@main2026-07-31 00:35:28 bob push.accepted backend-api@main2026-07-31 00:33:14 bob push.denied_path backend-api@main secrets/prod.env secrets-are-platform-only2026-07-31 00:33:14 bob push.rejected backend-api@main| Action | Means |
|---|---|
push.accepted | Authorization passed; the task was queued |
push.rejected | Refused; nothing was queued |
push.denied_path | One refused path, with the rule that refused it |
push.applied | The change landed on the forge |
pull.requested | A pull was queued |
pull.delivered | A filtered patch was produced |
Every record carries the policy version in force at the time, so a past
decision can be replayed against exactly the rules that produced it: find the
version, check out that commit of the policy repository, run
nitctl policy explain.
Following one operation end to end
Section titled “Following one operation end to end”Every request carries an id that appears in the API log, on the task, and in the audit trail. Given a developer’s error message:
nitctl audit -request 01J8Z3Q2M7C4V9K1That is the whole thread: what was asked, what was refused, which rule refused it, and which task it became.
Starting from a commit on the forge
Section titled “Starting from a commit on the forge”Every commit nit publishes carries trailers, so an investigation can start from the forge rather than from a developer’s report:
git log -1 --format='%(trailers:key=Nit-Request,valueonly)' <sha>nitctl audit -request 01J8Z3Q2M7C4V9K1Nit-User is the bundle identity, Nit-Policy-Version is the bundle that
authorized it — check out that commit of the policy repository and
nitctl policy explain replays the decision exactly.
Nit-Dropped appears only when the author used --drop-unauthorized. It is the
only signal on the forge that a commit is not what its author wrote, and worth a
scan when something upstream does not build:
git log --format='%h %an %(trailers:key=Nit-Dropped,valueonly)' | grep -v ' $'Nit trailers cannot be forged from a commit message — the worker strips any
Nit-…: line an author wrote before appending the real ones.
Records are append-only at the database level: a trigger refuses UPDATE,
DELETE and TRUNCATE with an error, so an application bug cannot rewrite
history.
Retention
Section titled “Retention”nitctl audit prune removes records older than a cutoff. It is the only way to
empty the table, and it goes to the database directly — there is no endpoint,
because nothing a request can reach may delete evidence.
nitctl audit prune -keep-days 365 # reports, deletes nothingnitctl audit prune -keep-days 365 -yes # deletesWithout -yes it counts and stops. There is no undo, and the audit trail is the
record that nit enforced anything.
cutoff: 2025-08-23T09:24:08Zmatched: 201 record(s) older than thatremoved 201 record(s)The purge records itself. Two rows survive it — audit.purge_started with
the cutoff and the count, and audit.purge_completed with what was removed —
naming whoever ran it. A purge interrupted halfway therefore leaves a started
with no completed, which is exactly what an auditor needs to see; the
alternative is a gap in the trail that nothing explains.
The append-only protection is restored afterwards. On PostgreSQL the guard
is lifted and restored inside each batch’s transaction, so no other session ever
observes it off. On MySQL and MariaDB that is not possible — dropping a trigger
is DDL and commits immediately — so there is a window, and a purge killed inside
it leaves the table unprotected. The next nitctl audit prune reports that
loudly and closes it:
WARNING: the append-only protection was already absent when this started.a previous purge did not finish, and audit_log has accepted deletions since.Rolling out a policy change
Section titled “Rolling out a policy change”- Pull request against the policy repository; CI runs
nitctl policy validate. - Merge; your deployment updates the checkout on each host.
- Every
policy.reload(30 s by default),nitdrereads it. curl /healthzreports the version in force.
A bundle that does not compile is not applied: the last good one stays in
force and the failure is logged loudly. During a rolling deploy, comparing
/healthz across replicas is how you spot two of them serving different bundles.
Things that go wrong
Section titled “Things that go wrong”| Symptom | Cause |
|---|---|
missing_patch on every push | nitd and the worker have different blob_dir. It must be one shared volume. |
Tasks stay queued | No worker running, or its -queues excludes that kind. |
| Tasks retry forever without failing | lease_duration is shorter than a clone takes. |
no sync token signing key | sync_key_file names a path that is not there, or the key is under 32 bytes. |
not found from nitctl stats | The account is not in admin_groups. |
A developer gets unknown_sync_point | Their workspace has never pulled. nit pull. |
A developer gets stale_sync_point | Their workspace is behind. nit pull. |
| A setting seems ignored | nitctl config show — the environment overrides the file. |
A branch that will not move
Section titled “A branch that will not move”nitctl tasks -repository backend-api -state runningIf a task has been running far longer than a clone should take, its worker is
probably gone. The lease will lapse within lease_duration and the reaper will
return it to the queue. If it does not, check that a reaper is running — it lives
in nitd, and reap_every governs it.
A push that keeps failing
Section titled “A push that keeps failing”nitctl tasks -state failed -json | head -40conflict means the patch no longer applies onto upstream. That is permanent —
retrying would conflict identically — so the task fails immediately rather than
burning its attempt budget. The developer pulls, resolves, and pushes again.
Backups
Section titled “Backups”| What | Why |
|---|---|
| PostgreSQL | Sync points, the audit trail, the queue |
| The policy bundle repository | Your authorization rules, with their history |
storage.blob_dir | Only in flight; patches are disposable once applied |
security.sync_key | Losing it makes every workspace resynchronize once |
Losing the database loses sync points, which means every workspace resynchronizes — survivable — and the audit trail, which is not. Back it up like the record it is.
storage.work_dir is scratch. Never back it up.
- The web console — the same data, in a browser.
- nitctl — every command.