Codr

How Codr is built

Codr is a git forge that one person can run and understand: one binary, one database file, one folder of bare repositories. This page explains the choices behind it and the rules the code keeps. Every choice has a numbered decision record (D1, D2, …) kept by the maintainers, with what it replaced and how to undo it; the numbers in code comments point to it.

One binary, git does the git

The forge is a single Go program, codr. It serves the web pages, the HTTPS and SSH git doors, the API and the hooks. It does not parse git's pack format or wire protocol itself. Clones and pushes run the real git binary (git upload-pack, git receive-pack, git http-backend), so every git client keeps working and git's own security fixes apply on the day they ship (D2).

Writes the web makes to a repository (merges, squash and rebase merges, wiki pages) use git plumbing on the bare repository: merge-tree builds the result in memory, commit-tree writes the commit, and update-ref moves the branch only if it still points where the page read it. There is no working tree, no lock file to clean up, and a branch that moved in between is refused rather than overwritten (D53, D83, D88).

One database file

Everything that is not git lives in one SQLite file in WAL mode: accounts, permissions, issues, pull requests, notifications, audit rows, settings (D3). Migrations only ever add. Repositories on disk are named by their database id (repos/<id>.git), never by a name a person typed, so renaming or a hostile name cannot reach the file system (D27).

Backups are consistent snapshots: the database through SQLite's VACUUM INTO, each repository as a verified git bundle, large files (LFS) and wiki repositories alongside. Every night the backup is restored into an empty folder and checked, because a backup that was never restored is a hope, not a backup (D41).

One permission decision

Who may read or write a repository is answered by one function, AccessFor, and every door asks it before git runs: SSH, HTTPS, the web pages and the API. A private repository a person cannot read answers "not found", never "forbidden", so its existence does not leak. Organisations and teams feed the same function (D74). Tests compare it with the list query that decides what a person sees, so the two cannot drift apart.

The same rule follows everything that shows content to someone else: notifications are only written for people who can read the repository, a profile's activity is filtered by the visitor's access, and an owner/repo#12 reference becomes a link only for readers of that repository (D68, D79, D87).

Push rules in one place

Protected branches, size limits and required checks are enforced by a git hook that calls back into the running server over a local socket, so the HTTPS and SSH doors share one implementation (D31). A protected branch can refuse force pushes, deletion, and, when asked, any commit whose CI checks have not all passed (D89).

Events, written once

Every change people care about (a push, an issue opened, a review, a merge) is written once to an outbox table, next to its audit row. Independent readers follow that outbox with their own cursor: webhook delivery (HMAC signed, retried, checked against private addresses after DNS resolution), notifications and email, and the reader that closes issues named by fixes #12 once the commit reaches the default branch. A slow reader never delays a push (D78, D79, D87).

CI outside the forge

codr-runner is a separate small program. It receives push webhooks, checks their signature, fetches the commit and runs the jobs listed in .codr/ci.toml, each in a container with no network, no capabilities, a read-only root file system and bounded memory, CPU and processes. Results come back as commit statuses through the API, with a link to the job log (D89). The forge does not run user code itself.

Secrets

Passwords are hashed with Argon2id. Session and access tokens are stored as hashes; a token is shown once. Secrets the server must use later (mirror tokens, webhook secrets, the mail password, backup keys) are sealed with a key that lives in the configuration file, not in the database, so a copy of the database alone opens nothing (D28, D38, D70). Sign-in can require a TOTP code; the step between password and code is a challenge, not a session (D80).

Text and settings

There are no user-facing strings in the code. Every sentence lives in locale/ms-MY.json and locale/en-US.json, and the build gate fails when a template or an error message carries literal text (D34). Values that reasonably change (page sizes, limits, timeouts, keyword lists) are settings in the database, editable without a deploy. Malay and English are equal from the first page.

The gate

Nothing is tagged until the full gate passes on that exact code tree: formatting, go vet, staticcheck, gosec, govulncheck, the race-detecting test suite, a size limit per file, the scan for hard-coded text, and real browsers (Chromium, and WebKit as an iPhone 13) in both themes with an accessibility check on every page. The deploy script refuses a tag whose tree has no record of a full green run, keeps the previous version, and rolls back on its own when the new one does not answer with its version (D42, D47).

What it does not try to be

Codr is not a hosting service for many tenants, does not try to match GitHub Actions feature for feature, and does not run an inbound mail server. Those limits are written down so they stay limits until there is a reason to change them.