Maintainer Mode
Sign in with GitHub
Work in progress. Maintainer Mode changes every day: pages, numbers and links can change or break. Tell us what is wrong or missing.

Run it

All docs · The source is docs/operations.md.
nix build
GITHUB_TOKEN=… ./result/bin/maintainer-mode sync --db club.sqlite --index
./result/bin/maintainer-mode serve --db club.sqlite
VariableMeaning
MAINTAINER_MODE_LISTENlisten address, default 127.0.0.1:8790
MAINTAINER_MODE_DBSQLite path
MAINTAINER_MODE_PUBLIC_URLthe public URL; the OAuth callback is <url>/auth/callback
GITHUB_CLIENT_IDOAuth app client id
GITHUB_CLIENT_SECRET_FILEfile holding the OAuth app client secret
MAINTAINER_MODE_BOT_TOKEN_FILEclassic token of the bot account (scope notifications)
GITHUB_APP_IDGitHub App whose installation token syncs first
GITHUB_APP_KEY_FILEfile holding the PEM private key of the GitHub App
GITHUB_APP_INSTALLATION_IDinstallation to use; else the one on the account of the App
MAINTAINER_MODE_GITHUB_APP_SLUGthe App people install on their workflow repository
MAINTAINER_MODE_UPLOAD_TOKENthe client's upload token for submit
MAINTAINER_MODE_CHANNELSchannels next to master, default nixos-unstable,stable
MAINTAINER_MODE_S3_ENDPOINTS3-compatible endpoint for the copy of the event log
MAINTAINER_MODE_S3_BUCKETthe bucket of that copy
MAINTAINER_MODE_S3_REGIONthe region that the request signature names
MAINTAINER_MODE_S3_CREDENTIALS_FILEfile with the access key id and the secret key
MAINTAINER_MODE_ARCHIVE_REPOSrepositories to archive; default NixOS/nixpkgs
MAINTAINER_MODE_ARCHIVE_ITEMSitems the archive walk fetches per repository and pass
MAINTAINER_MODE_RESCANtrue turns on the server rescans of public flakes
MAINTAINER_MODE_RESCAN_BWRAPthe bwrap program, default bwrap
MAINTAINER_MODE_RESCAN_PRLIMITthe prlimit program, default prlimit
MAINTAINER_MODE_RESCAN_NIXplain nix in /nix/store, default nix from PATH
MAINTAINER_MODE_RESCAN_MAX_FLAKESflakes a person can register, default 3
MAINTAINER_MODE_RESCAN_PER_DAYscans a person gets in 24 hours, default 4
MAINTAINER_MODE_RESCAN_TIMEOUTwall-clock limit of one scan in seconds, default 900
MAINTAINER_MODE_RESCAN_MEMORY_MBaddress-space limit of each sandboxed process, default 3072
MAINTAINER_MODE_RESCAN_PER_DAY_TOTALscans in 24 hours for all people together, default 100
MAINTAINER_MODE_SMTP_HOSTSMTP server for the email digest; off without it
MAINTAINER_MODE_SMTP_PORTSMTP port, default 465 (TLS); another port needs STARTTLS
MAINTAINER_MODE_SMTP_USERSMTP user, with the password file
MAINTAINER_MODE_SMTP_PASSWORD_FILEfile holding the SMTP password
MAINTAINER_MODE_SMTP_FROMthe From address of the digest
MAINTAINER_MODE_MAIL_PER_DAYemails the server sends in 24 hours, default 500

CI is the flake's checks: nix flake check.

UX audit

ux-audit opens each page of the site in a headless Chromium. It takes full-page screenshots and checks each page for common faults. The page list is nix/ux-pages.txt. A unit test fails when a GET route is not in this list.

To audit a fresh local server, run:

nix run .#ux-audit -- --local

--local starts maintainer-mode on a temporary database and a free port. It blocks the outbound network of the server. It adds a test session, so it audits the pages signed out and signed in. The database is empty, so package and module pages answer 404.

Add --seed to fill the temporary database before the audit. The tool writes a small nixpkgs index, some upstream releases, a machine report and some watches for the test session. The signed-in lists of /packages and the home page then show rows.

To audit a running server, run:

nix run .#ux-audit -- --url https://maintainermode.org --only-public

To audit the signed-in pages of a running server, set MAINTAINER_MODE_AUDIT_SESSION to the value of your session cookie. The tool does not write or print this value.

The tool writes to ./ux-audit. Use --out to change the folder. Open index.html in it. The page shows a table of issues, most severe first, and four screenshots per page: 390x844 and 1280x900, light and dark. report.json holds the same data.

OptionMeaning
--pages <regex>audit only the pages whose path matches
--only-publicskip the signed-in pages
--set NAME=VALUEchange a sample value of the page list, for example --set pr=123
--link-cap <n>check at most n same-origin links, default 300
--bin <path>the binary for --local; in the dev shell, the default is target/debug

The checks are: the HTTP status, the HTML size, the load time, console errors, failed requests, horizontal overflow, images without alt, form controls without a label, controls without a name, the order of the headings, duplicate ids, low text contrast, small tap targets on a phone, and same-origin links that answer 404. The link check does not open /pr/, /claude/, /auth/ and /feed/ links.

Email

Set MAINTAINER_MODE_SMTP_HOST and MAINTAINER_MODE_SMTP_FROM to turn the email digest on. MAINTAINER_MODE_SMTP_PORT is 465 by default. Port 465 uses TLS from the start. Any other port must offer STARTTLS. Set MAINTAINER_MODE_SMTP_USER and MAINTAINER_MODE_SMTP_PASSWORD_FILE together, or leave both unset. In the NixOS module, use services.maintainer-mode.smtp. The password file goes through LoadCredential.

Email limits

The server sends at most 500 emails in 24 hours: digests, test emails and confirmations together. To change the limit, set MAINTAINER_MODE_MAIL_PER_DAY or the module option services.maintainer-mode.smtp.perDay. Each person gets at most 3 confirmation emails and 3 test emails in 24 hours. Over a limit, the server does not send the email and writes a warning to the log. The Data status page (/state?tab=status) shows the emails sent and skipped in the last 24 hours. It shows no address and no login.

New secrets without a restart

The service reads each secret file again when the file changes. It checks each file at most once a minute. The log names the file that changed. The log never shows the secret.

systemd copies each secret file when the service starts (LoadCredential). The NixOS module adds a timer, maintainer-mode-credentials.timer. Each minute, the timer compares each secret file with its copy. When a file changed, the timer reloads maintainer-mode.service. The reload copies the secrets again (RefreshOnReload=credentials, systemd 260 or later). The service does not restart. With an older systemd, the service restarts.

A new secret takes effect within two minutes after the deploy. You do not have to restart the service. This works with sops-nix, clan vars and plain files.

To see when a secret changed, run journalctl -u maintainer-mode-credentials -u maintainer-mode | grep changed.

What we took, and under which licence

PackageVersionLicenceUsed for
@primer/css22.3.2MITdist/primer.css, trimmed to the classes in src/ and to the light and dark themes
@primer/octicons19.40.0MIT16px SVG icons, sent as one hidden sprite on each page
nixfresh nix.rs and attrs.rs2026-10MITsrc/eval.rs and src/attrs.rs: the evaluator wrapper and the attribute index, copied and trimmed
nixfresh repology and tracker collectors2026-10MITsrc/health.rs: the bulk reads of repology and the security tracker, rewritten for one host
nixfresh upstream.rs, version.rs and direct collector2026-10MITsrc/upstream.rs and src/version.rs: upstream releases from tags and registries
nixfresh score.rs2026-10MITsrc/score.rs: the risk score, simplified
nixfresh osv collector2026-10MITsrc/osv.rs: OSV matches for the exact version of PyPI, crates.io and npm packages
nixfresh cve, nvd and epss collectors2026-10MITsrc/severity.rs: CVSS, CISA exploitation and EPSS for the tracker's CVEs
nixfresh nix.rs and report.rs2026-10MITsrc/scan.rs and src/report.rs: the closure scan, the local report format, trimmed

Both are fixed-output fetches of the npm tarballs, pinned by their sha512 (nix/primer.nix), and their licence files ship with the assets. The pages are server-rendered HTML; there is no JavaScript.

Page weight and caching

Version 22 of @primer/css no longer ships Box, Label, UnderlineNav, Subhead or flash. src/club.css rebuilds those few on Primer's own color tokens, so light and dark mode follow the bundle.

The hues are the NixOS brand palette from NixOS/branding (colors.toml, OKLCH): Afghani Blue for links, buttons and the header, Argentinian Blue for focus and the current tab, and the accent colors for pass, pending, fail and done. src/club.css sets them on Primer's tokens, and Primer keeps its grays. The site uses the colors only, not the NixOS logo, and it is not an official NixOS project.

Primer is GitHub's design system, used so that people who arrive from a pull request page do not learn a second interface. The club is an independent community project. It is not a GitHub product and is not endorsed by GitHub, and it does not use GitHub's name or logo as its own.