- Feature Name: `namespaced-repositories` (a repository is named within its
namespace, a person's or a team's, not across the forge; a permanent ID
is its key and namespace/name is its address; flat names stay a
deployment option for single-user builds)
- Start Date: 2026-10-21
- Status: Draft — decisions settled by the owner on 2026-10-21 (see
[Decisions](#decisions)); rungs 1 (schema) and 2 (addresses) built
- Depends on: RFC 0001 §2 (store access), §7 (sync fronting), §9 (the ops
layer generates the API); RFC 0010 (local mode); RFC 0015 (companions);
RFC 0018 (teams: the shared pool of account and team names, the
namespace a repository appears under, repo_place_past)
## Summary
[summary]: #summary
RFC 0001 chose a flat repository namespace, and it is still flat underneath.
RFC 0018 gave every repository a namespace, the person or team it appears
under, and `/r/dmestas/quarry` resolves; but the name after the namespace is
still the forge-wide key of the repository. There can be one `tools` on the
whole forge. Every person and team competes for every name, and a forge that
grows runs out of good ones.
This RFC splits a repository's **key** from its **address**. The key is a
permanent ID that every table, store path, route and record refers to, and
that never changes. The address is namespace/name, unique within its
namespace: `dmestas/tools` and `some-team/tools` are two repositories.
Moving a repository between namespaces (a person to a team, a mirror to its
steward) or renaming it changes one row; nothing keyed by it is rewritten.
Every existing repository's ID is its current name. Those names are unique
today, so every column that holds one, every store path, and every clone
URL already in use stays valid with no data rewritten. Only repositories
made after this lands get generated IDs.
Flat names remain a deployment option: a single-user build (`quarry ui`, and
the WebAssembly build that will run as `zit ui`) has one person and no
namespaces, and addresses repositories by name alone.
## Decisions
[decisions]: #decisions
Settled by the owner on 2026-10-21:
1. **The key is a permanent ID**, not the address string. A move or a
rename touches one row, and no grant, link, CI record or store path has
to follow it.
2. **A bare `/r/<name>` redirects when it is unique.** Exactly one
repository with that name: redirect to its namespaced address, so every
link saved so far keeps working. Several: a short page listing them.
New links always carry the namespace.
3. **Forge-held repositories live in one forge namespace, `zeitforge`.**
The mirrors nobody owns (tcl, tk, sqlite, fossil, netbsd-src, pkgsrc and
the rest) appear as `zeitforge/tcl`. The name is reserved: no account or
team can take it. A mirror may later be **stewarded** by a person or
team; it then moves into their namespace like any other move, and its
`zeitforge/` address keeps redirecting.
4. **Teams are namespaces exactly as people are.** A team's slug scopes
names the way a person's handle does. The deed stays with a person
(RFC 0018: dissolving a team never strands code); this RFC does not
change who holds the deed.
5. **Namespacing is a deployment option.** Flat names for single-user
builds; namespaced for a forge.
## Design
[design]: #design
### Key and address
The `repo` table's `name` column stays the key column, so none of the 66
columns across 49 files that refer to a repository change type or meaning:
they hold the ID. Two columns carry the address:
- `namespace` (exists since RFC 0018): an account handle, a team slug, or
`zeitforge` for a forge-held repository.
- `handle` (new): the repository's name within its namespace.
`UNIQUE (namespace, handle)` replaces the forge-wide uniqueness of the
name. For every row that exists when this lands, `handle = name`.
A new repository's ID is minted when it is created, in a shape the account
and team name rule refuses, so an ID can never be read as a namespace in a
path. It is never shown as the repository's name; a reader sees the
address.
A new store is created at `<repos-root>/<id>`. Existing stores keep the
path their row already records.
### Addresses on the wire
Routes are not rewritten. The forge already reads `/r/<namespace>/<repo>`
before its routes match (RFC 0018); that reading now resolves the pair to
the ID, and the 173 web routes and 164 API routes continue to receive
`{repo}` as they do today. The same resolution applies to:
- **web pages:** `/r/<namespace>/<handle>/...`
- **the API:** `/api/v1/r/<namespace>/<handle>/...`
- **sync:** `fossil clone` and `zit clone` of `/r/<namespace>/<handle>`
- **links the forge writes:** every link is built from the address, never
the ID (the `repoURL` helper already builds namespaced links)
A first segment is read as a namespace when the second segment is a
repository in it, and otherwise as a repository: the pools stay disjoint as
RFC 0018 requires, now between namespaces and IDs rather than namespaces
and names.
### The bare form
`/r/<x>` with no namespace resolves in this order:
1. `x` is an ID: that repository. Every existing repository's ID is its
name, so every remote cloned from `/r/quarry` keeps syncing forever,
whatever is created later.
2. `x` is the handle of exactly one repository: redirect a page to its
address; answer sync and the API as that repository.
3. `x` is the handle of several: a page listing them. Sync and the API
are refused with a message that names each namespaced address.
4. Otherwise: not found.
Moves already redirect through `repo_place_past` (RFC 0018); a rename adds
the old handle to the same memory, so an address that was ever valid keeps
leading to the repository that held it.
### Creating a repository
The new-repository form gains **Owner**: the reader, or one of their teams
where they may create. The name is checked within that namespace as it is
typed (#979 checks it forge-wide; this narrows the check), and an agent
creating a repository places it in its person's namespace, where the deed
already goes (#969).
### Flat mode
`quarry serve --repo-names=flat|namespaced`, namespaced by default. In flat
mode the address is the handle alone, `UNIQUE (handle)` holds forge-wide,
links are `/r/<handle>`, and the form has no owner field. `quarry ui` and
the WebAssembly `zit ui` build run flat. The key and the stores are the
same in both modes, so a repository moves between a flat build and a forge
without changing anything but how it is addressed.
## Migration
[migration]: #migration
Each rung lands on its own and leaves the forge working.
1. **Schema, no behaviour change.** Add `handle`, backfilled from `name`
and written by every insert; add `UNIQUE (namespace, handle)` beside the
existing uniqueness; reserve `zeitforge` for accounts, teams and
repositories alike. (Built.)
2. **Addresses resolve to IDs.** The `/r/<namespace>/<handle>` reading,
the API and sync forms, the bare-form rules and the listing page
(`/named/<handle>`); every link the forge builds is the address.
(Built.) Rename memory waits for renames, which do not exist yet.
3. **Creation is per namespace.** Minted IDs and `<repos-root>/<id>`
stores; the owner field; the name check within the namespace; the
forge-wide uniqueness of `name` stops applying to handles. Two things
rung 2 leaves to it. Pages that print a repository's name print its
key, which is its handle for every repository until this rung mints
keys that are not; they print the handle from here. And an address is
read as readers see it: a private team's repositories appear under
their owner while it is private, so the name check covers the owner's
namespace too, or a team going private could put two repositories at
one address.
4. **Mirrors move to `zeitforge/`.** Stewardship is a move into the
steward's namespace. The start-up repair that holds a namespace to its
team or owner (`repoMigrations`) learns that a repository with no owner
appears under `zeitforge`; as it stands it would clear that namespace
on the next start.
5. **Flat mode** for `quarry ui` and the WebAssembly `zit ui` build, and
`--repo-names=flat|namespaced` with it: the switch arrives when there
is something for it to switch.
## Not in this RFC
- **Renaming as a feature.** The key makes it one row and the rename memory
is specified above; the control that offers it is separate work.
- **Team-held deeds.** Who holds the deed is RFC 0018's rule and stays as
it is.
- **zit's own naming.** A zit store does not know the address it is served
under, so nothing in zit changes.