- 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); nothing here is 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
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
Settled by the owner on 2026-10-21:
- 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.
- 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. - Forge-held repositories live in one forge namespace,
zeitforge. The mirrors nobody owns (tcl, tk, sqlite, fossil, netbsd-src, pkgsrc and the rest) appear aszeitforge/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 itszeitforge/address keeps redirecting. - 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.
- Namespacing is a deployment option. Flat names for single-user builds; namespaced for a forge.
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, orzeitforgefor 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 cloneandzit cloneof/r/<namespace>/<handle> - links the forge writes: every link is built from the address, never
the ID (the
repoURLhelper 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:
xis an ID: that repository. Every existing repository's ID is its name, so every remote cloned from/r/quarrykeeps syncing forever, whatever is created later.xis the handle of exactly one repository: redirect a page to its address; answer sync and the API as that repository.xis the handle of several: a page listing them. Sync and the API are refused with a message that names each namespaced address.- 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
Each rung lands on its own and leaves the forge working.
- Schema, no behaviour change. Add
handle, backfilled fromname; give forge-held repositories thezeitforgenamespace and reserve the name; addUNIQUE (namespace, handle)beside the existing uniqueness; add--repo-names, defaulting to namespaced. - Addresses resolve to IDs. The
/r/<namespace>/<handle>reading, the API and sync forms, the bare-form rules and the listing page, and rename memory inrepo_place_past. Pages show the handle. - Creation is per namespace. Minted IDs and
<repos-root>/<id>stores; the owner field; the name check within the namespace; the forge-wide uniqueness ofnamestops applying to handles. - Mirrors move to
zeitforge/. Stewardship is a move into the steward's namespace. - Flat mode for
quarry uiand the WebAssemblyzit uibuild.
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.