Branching & Namespaces

"Branching" in Relata refers to three related but distinct surfaces that the rest of the docs touch separately. This page unifies them under one vocabulary so the three API names (which are easy to conflate) are unambiguous.

SurfaceWhat it forksGranularitySpeedAPI
Namespace branchAn entire data namespace — every type and every rowWhole namespaceO(types), constant time (copy-on-write; no row copy)BRANCH <name> FROM <source> (SQL) · POST /v1/namespaces/{name}/branch (HTTP)
Schema branchThe ontology/schema only (git-branched ontology)Schema onlyO(types)POST /schema/branches/{name} (HTTP)
Sub-tenant namespaceA hierarchical path within a tenantPer-row NamespacePath fieldEnforced on read/writeX-Organization-Id: acme/eu/hr + query predicates

The naming collision, resolved. BRANCH ... FROM ... and POST /v1/namespaces/{name}/branch fork the whole data namespace (types + rows) — they are the same operation exposed over two doors. POST /schema/branches/{name} forks the schema/ontology only. They are not interchangeable.

Namespace branching (data fork)

Relata can fork an entire namespace — every registered type and every row — into a new named branch in constant time, regardless of how much data the source branch holds. Writes on either branch are isolated: the fork shares its parent's existing data via copy-on-write, and new writes on each branch go to that branch's own active segment.

This is the same primitive that backs POST /schema/branches/:name for the schema-only case. A namespace fork is O(types), not O(rows).

SQL

BRANCH dev FROM main

BRANCH <name> FROM <source> requires no PURPOSE clause — like DELETE and UPSERT, it is namespace management, not a governed data read/write. <name> and <source> accept a bare identifier or a quoted string.

HTTP

curl -X POST http://localhost:9090/v1/namespaces/dev/branch \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"branch_from": "main"}'
{ "created": true, "branch": "dev", "source": "main" }

Returns 409 Conflict if dev already exists, 404 Not Found if main (the branch_from source) does not exist, and 400 Bad Request if the branch name is empty or over 128 characters.

Listing and deleting branches

Branches created either way are ordinary Relata branches: they show up in GET /schema/branches, and DELETE /schema/branches/:name removes them. main cannot be deleted.

curl http://localhost:9090/schema/branches \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN"
 
curl -X DELETE http://localhost:9090/schema/branches/dev \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN"

How the fork works (constant time)

  • Creating a branch wraps every existing row in the source branch's tables in a shared, immutable Arc slab — no row is copied. A 10-row namespace and a 10-billion-row namespace fork in the same amount of time.
  • A write to either the source or the new branch after the fork goes into that branch's own fresh active segment. The parent's data is never mutated, so reads on one branch never see writes made on the other.
  • Branch-from-a-branch (BRANCH c FROM b where b was itself forked from a) works the same way, any number of levels deep.
  • Deleting a branch frees its tables and evicts its partition-lock entries.

Canonical use-cases

  1. Per-developer sandbox — give every developer (or every VCS feature branch) an isolated, full copy of production data, without an O(rows) copy: BRANCH alice-feature-142 FROM main. Discard with DELETE /schema/branches/alice-feature-142.
  2. CI test pipeline — fork a fresh branch per test run from a known-good fixtures branch, run the suite's writes against it, discard it — a clean, isolated dataset per run without restoring a snapshot from disk.
  3. Point-in-time snapshotBRANCH pre-migration-2026-08-01 FROM main immediately before a risky bulk migration or schema change, for an instant rollback target that needs no WAL replay or backup restore.
  4. Codebase / RAG indexing — run multi-pass indexing jobs against a scratch branch so partial or failed runs never corrupt the namespace other consumers read from.

Schema branches (git-branched ontology)

A schema branch forks the ontology only — develop schema changes (new types, state-machine constraints, computed columns) on a branch without affecting production, then merge or discard.

# Create a schema branch
curl -X POST http://localhost:9090/schema/branches/dev-schema \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
  -d '{"from": "main"}'
 
# ... make schema changes on the branch, merge when ready ...
 
# Delete if discarded
curl -X DELETE http://localhost:9090/schema/branches/dev-schema \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN"

Note the body key is "from" here (not "branch_from" as on the namespace-fork door) — these are two different endpoints. See Ontology & Schema.

Sub-tenant namespaces

A NamespacePath partitions data within a tenant — for example, separating departments or cases inside one organisation — expressed as /-separated paths on the tenant identity:

acme           ← top-level tenant
acme/eu        ← regional sub-tenant
acme/eu/hr     ← team sub-tenant (acme/eu sees acme/eu AND acme/eu/hr)
acme/us        ← separate regional sub-tenant (does NOT see acme/eu)
# Write into a sub-tenant
curl -X POST "http://127.0.0.1:9090/ingest?object_type=Employee&purpose=hr" \
  -H "Content-Type: text/csv" \
  -H "X-Organization-Id: acme/eu/hr" \
  --data-binary $'name\nBob'

Honest status: sub-tenant namespace enforcement is partially wired — the Row.namespace_path field exists but is not universally populated on the write path today. Do not rely on namespace filtering as a hard security boundary yet; use tenant-level (X-Organization-Id) isolation for hard boundaries. See Multi-Tenancy for the current state.

Cache pinning for namespaces

A namespace can be pinned in the cache tier (RELATA_PINNED_NAMESPACES) so it reserves a dedicated NVMe slice and is evict-immune under pressure. A branch of a pinned namespace starts unpinned (exact-match membership, not prefix). See Environment Variables.

See also