Accounts
Invite-only registration on a Noite install — bootstrap admin, single-use codes, and admin panels.
Registration is invite-only once the instance is bootstrapped. The first account bootstraps it, and on a public base it must prove it is the operator: with NOITE_ADMIN_EMAIL set, only that address may register first and it must still present the one-time setup code printed at boot (knowing the address alone never suffices — it is promoted to the admin role on success); without it, the first registration must present the code alone (see Install). On a localhost-family base the first account needs no proof and just becomes admin. Every later registration must present a single-use invitation code. Each new account receives INVITES_PER_USER = 2 codes of its own, so an invitee can pass one on without admin rights.
Setup codes are 20 characters in four-char groups (ABCD-EFGH-JKLM-NPQR-STUV) drawn from an unambiguous alphabet (2^100 codes); only the SHA-256 hash is stored, a code works once, and the claim spends it atomically with the admin grant, so one code admits exactly one account even under concurrent signups. A lost code is reissued on the server (docker compose exec noite noite-runner setup-code — the old code dies). Invitation codes are 12 characters in four-char groups (ABCD-EFGH-JKLM) from the same alphabet, and a code that is used, revoked or unknown is refused with a specific message before any account is created. Redemption is a single atomic claim (UPDATE ... WHERE usedBy IS NULL), so one code admits exactly one account even under concurrent signups.
- Members read their unused codes in the “Invitations” card on
/account. - Admins mint 1–50 more and revoke unused ones from the Invites tab of the admin home (the control app’s detail page at
/apps/_control). - The signup form shows the setup-code field on a fresh public install (in both modes — the reserved address must still present the code), the invitation-code field only when
GET /api/invite/statusreports the instance is past its first account.
NOITE_ADMIN_EMAIL reserves the first account by address — which must still present the setup code — and promotes the matching account to the admin role at startup; it is documented in Environment variables.
First-run onboarding
A new account sees a one-time three-step tour the first time it reaches the dashboard: create an app and push to its Git remote, mint an API token (keys live on /account — used as the Git HTTPS password with username git, or as NOITE_API_KEY for the CLI), and where to follow the project. Finishing, skipping, or closing the modal marks the account done (user.onboardedAt) and it never returns. An account that signed in before this release sees the tour once and is then marked the same way; no operator action is needed.
Lost passkey
On the login screen, Lost passkey? emails a six-digit code (valid 10 minutes, five attempts) and, once entered, signs the account in on the page where you add a new passkey. Emailing needs NOITE_EMAIL_WEBHOOK_URL: the control UI POSTs { "email", "otp", "from", "type": "sign-in" } to it, and whatever you point it at (a mail relay, an automation) delivers it. Without a webhook nothing is sent on a real domain: the control UI logs the failure, and the screen still says a code was sent. Use the command below.
An operator who cannot wait for email, or never set a webhook, mints the same code on the server:
cd /opt/noite
docker compose exec noite noite-runner recover # for the instance owner (oldest admin)
docker compose exec noite noite-runner recover --email [email protected]
It prints a code and where to enter it. The code is the same kind the email carries: it works once, expires after 10 minutes, and anyone holding it can sign in as that account, so treat the output like a password. Running it needs a shell in the container, which already holds every secret of the install, so it grants nothing that access did not.
The admin home
An instance admin sees the control plane itself in /apps: the Noite card (“admin”), which is not a user app but a link to a dedicated detail page. That page is the whole admin home — the control plane’s telemetry tabs (Overview, Metrics, Errors, Logs) next to instance administration (Users, Apps, Invites) and the instance switches (Settings: anonymous telemetry, release check), with the control database linked from Overview. /account is personal settings only: profile, passkeys, API keys, the account’s own invite codes and agent connect. Every tab re-checks the same rule server-side: a real instance admin, never an impersonated session.
Ban in the Users tab revokes the account’s sessions and deletes its API keys, so git push, event ingest and /mcp stop immediately. The ban is checked on every request as well, so a key that somehow survived the deletion is still refused. Unbanning does not restore a deleted key: the account mints a new one.
In make dev the control UI runs under vite and supervises no celld, so Metrics, Errors and Logs are hidden — there is no control fleet to record them — while the Users, Apps and Invites tabs and the control database still work.
Browsing the control database
The Overview tab lists the control database; it opens on its own page (/storage/_control/d1:noite-control) in the same D1 grid tenant apps use, served in-process from the control node’s own database — the worker already owns that binding, so nothing shells out to the celld d1 CLI (which once hung every later page load; see SPEC.md). Each row also links to the proper admin action: a user row offers Manage in Users and an invite row Manage in Invites, pre-filling that tab’s search with the row’s email or code.
The browser is deliberately narrow:
- Values that would leak credentials are masked and never leave the worker:
session.token,account.password/accessToken/refreshToken/idToken,verification.value,passkey.publicKey/credentialID,apikey.key. session,account,verification,passkeyandapikeyrows cannot be inserted or edited. The one exception is deleting asessionrow by primary key, which revokes that session.user,invite,app_collaboratorandcollaborator_inviterows can be added, edited and deleted — exceptuser.role,user.banned,user.banReasonanduser.banExpires(change roles and bans in the Users tab). You can edit your ownuserrow (e.g. clearonboardedAtto see the tour again) but not delete it, so an admin cannot lock themselves out.- Everything else is read-only by default, including tables a later release adds, so new auth state can never become silently writable.
- The browser is unavailable while impersonating, and every read and write re-checks the admin gate and this policy server-side.