The first version of every content store looks like this:

PUT /blobs/<digest>
GET /blobs/<digest>

That is a good start.

It is not yet a registry.

A registry has to answer questions that a bucket does not:

  • Which name points to this object now?
  • Which objects compose this application?
  • Which signatures and attestations refer to it?
  • Which namespace owns the mutable reference?
  • Can several related objects become visible atomically?
  • How do two replicas discover what they disagree about?
  • Which unreachable objects can be collected safely?

Once content has stable identity, the interesting state moves into the relationships around it.

That is why I think of kappa-registry as a graph service with a blob store underneath—not a blob service with some metadata attached.

Global content and namespace meaning are different

An immutable blob can be global.

If two namespaces store the same bytes under the same hash axis, the content object should exist once. The namespaces may still disagree about what the object means, which tag points to it, or which edges are trusted.

flowchart TB
    B[Global content-addressed blobs]

    N1[Namespace alpha] --> T1[tag latest]
    N1 --> E1[typed edges]
    N1 --> R1[signed root]

    N2[Namespace beta] --> T2[tag stable]
    N2 --> E2[different edges]
    N2 --> R2[signed root]

    T1 --> B
    E1 --> B
    T2 --> B
    E2 --> B

This separation prevents a mutable name from becoming part of immutable object identity.

The blob says:

These bytes derive this address.

The namespace says:

In this context, this mutable name or relationship currently refers to that object.

Those are different authorities and should have different storage semantics.

Tags are pointers, not identities

Humans need names such as:

latest
production
model-small
release-2026-08

Those names are useful precisely because they can move.

A content address is useful precisely because it cannot.

flowchart LR
    T[Mutable tag: production] --> A1[Object κ1]
    T -. update .-> A2[Object κ2]
    A1 --> B1[Immutable bytes]
    A2 --> B2[Immutable bytes]

The update should be conditional. A client can say:

Move production from κ1 to κ2 only if it still points to κ1.

That is compare-and-swap for namespace state. Without it, two publishers can overwrite each other while each believes it advanced the same tag.

The content store remains append-only. The tag update is the contested mutable operation.

Objects become useful through typed edges

A model artifact is rarely one blob.

It may have:

  • weight shards
  • a tokenizer
  • a configuration
  • an evaluation report
  • a license
  • an attestation
  • a compiled graph
  • a compatibility declaration

A registry can encode those relationships as typed edges:

flowchart LR
    M[Model manifest κ] -->|weights| W[Weights κ]
    M -->|tokenizer| T[Tokenizer κ]
    M -->|config| C[Config κ]
    M -->|evaluated-by| E[Evaluation report κ]
    M -->|compiled-as| H[.holo archive κ]
    H -->|requires| W

The edge type matters. “Depends on,” “was derived from,” “is signed by,” and “supersedes” are not interchangeable links.

Typed edges let a registry answer questions such as:

  • return every attestation referring to this artifact
  • walk the dependency closure of this application
  • find all compiled archives derived from these weights
  • compare two namespace graphs
  • collect objects not reachable from any retained root

A bucket can store an edge document. A registry makes the edge queryable and part of its consistency model.

The namespace needs a root

A namespace contains mutable state:

  • tags
  • symbolic references
  • edges
  • schema declarations
  • filters
  • pinned roots
  • transaction results

The registry can derive a deterministic root from the canonical namespace state.

Let NN be the ordered set of namespace records. A simplified root is:

ρN=H(canon(N))\rho_N = H(\operatorname{canon}(N))

In practice, the registry may use a tree or range structure so parts of the namespace can be reconciled efficiently. The important property is that the same logical state derives the same root.

flowchart TD
    T[Tags] --> C[Canonical namespace state]
    E[Edges] --> C
    S[Schemas and filters] --> C
    P[Pins] --> C
    C --> R[Deterministic root]
    R --> G[Optional signature]

Signing the root does not make every object trustworthy. It makes the namespace state attributable and tamper-evident.

A client can verify that the tag and edge graph it received belongs to the signed state it expected.

Multi-object changes need atomic promotion

Suppose publishing an application requires:

  1. uploading three blobs
  2. storing a manifest
  3. adding dependency edges
  4. moving a release tag

If the process fails after step two, readers can observe an incomplete object graph.

A registry needs a transaction boundary.

sequenceDiagram
    participant C as Client
    participant S as Staging area
    participant R as Registry namespace

    C->>S: begin bounded transaction
    C->>S: upload blobs and manifest
    C->>S: add edges and tag update
    C->>S: validate complete set
    alt all checks pass
        S->>R: atomic promotion
        R-->>C: new namespace root
    else limit, timeout, or validation failure
        S-->>C: abort
    end

The staging area needs its own controls:

  • maximum concurrent transactions
  • maximum staged bytes
  • expiration
  • validation before promotion
  • cleanup after abandonment

Atomicity without bounds becomes an easy way to consume storage indefinitely.

Resumable upload is part of object integrity

Large artifacts fail mid-transfer.

A robust upload protocol needs:

  • ordered chunks
  • recovery state
  • hard body limits
  • an expiration window
  • final digest verification
  • refusal when the URL address and computed address disagree

The final step is what turns an upload session into a content-addressed object.

stateDiagram-v2
    [*] --> Open
    Open --> Receiving: PATCH ordered chunk
    Receiving --> Receiving: more chunks
    Receiving --> Finalizing: PUT with expected address
    Finalizing --> Complete: computed address matches
    Finalizing --> Rejected: mismatch
    Open --> Expired
    Receiving --> Expired

Until finalization, the bytes are staging data. After verification, they can enter the global immutable store.

Reconciliation should compare state, not replay every write

Two registries or replicas need to discover which objects and namespace records differ.

Replaying every historical operation is one approach. Comparing deterministic state summaries is often simpler.

If two namespace roots match, the logical states match under the root construction:

ρA=ρBNA=NB\rho_A = \rho_B \Longrightarrow N_A = N_B

up to the collision bound and the canonical state model.

If roots differ, a range-based reconciliation structure can narrow the disagreement instead of transferring the entire namespace.

flowchart TD
    A[Replica A root] --> Q{Equal to B root?}
    B[Replica B root] --> Q
    Q -- yes --> D[No reconciliation needed]
    Q -- no --> R[Compare ranges / subroots]
    R --> M[Identify missing records]
    M --> F[Fetch blobs and namespace updates]
    F --> V[Verify and recompute root]

Content objects remain easy to replicate because their addresses verify them on receipt. Mutable namespace state needs the additional root and update protocol.

Garbage collection is a graph operation

Deleting every unpinned blob is unsafe.

A pinned manifest may refer to an unpinned weight shard. A signed namespace root may retain objects through edges. A delta bundle may depend on a base bundle.

The live set is the reachability closure of retained roots:

L=closure(RpinsRnamespacesRleases)L = \operatorname{closure}(R_{\text{pins}} \cup R_{\text{namespaces}} \cup R_{\text{leases}})

An object is collectible only if it is not in LL and no active transaction or retention policy protects it.

flowchart LR
    P[Pins] --> W[Reachability walk]
    N[Namespace roots] --> W
    X[Active leases / transactions] --> W
    W --> L[Live object set]
    A[All stored objects] --> D[Set difference]
    L --> D
    D --> G[Garbage-collection candidates]

The graph is not metadata used after storage. It determines storage safety.

Bundles are graph transport

A portable bundle should carry a closure of related objects, not an arbitrary directory dump.

A full bundle can contain:

  • root objects
  • all missing reachable blobs
  • namespace records
  • edge records
  • integrity metadata

A delta bundle carries only what the receiver lacks relative to a known root or fingerprint.

This is useful for offline transfer, air-gapped deployment, browser import, and peer synchronization. The bundle remains verifiable because every object has an address and the namespace state has a deterministic root.

Policies belong at admission boundaries

A registry can support schemas and filters that decide which manifests, edges, or object types may enter a namespace.

That is different from the content hash.

The hash says the bytes match the address. The admission policy says the namespace is willing to reference them.

flowchart LR
    U[Uploaded object] --> H{Address verifies?}
    H -- no --> X[Reject]
    H -- yes --> S{Schema / filter passes?}
    S -- no --> Y[Keep out of namespace]
    S -- yes --> N[Admit reference and update root]

A globally valid blob can still be invalid for a particular namespace.

This keeps policy contextual without making the object identity contextual.

A single-node registry is still useful

The current kappa-registry implementation is explicit about its scope: filesystem-backed, single-node, with process-local upload sessions and rate-limiter state, and an authorization hook that is not yet a complete external identity system.

That does not make it a toy.

A single-node reference implementation can establish:

  • protocol behavior
  • object verification
  • namespace separation
  • transaction semantics
  • graph queries
  • root derivation
  • bundle formats
  • garbage-collection rules
  • conformance tests

Distribution can then be added around a clear state model rather than used to discover the state model in production.

The dangerous order is to build a cluster first and decide later what replicas are supposed to agree on.

The blob is the easy part

Stable object identity simplifies storage. It does not eliminate mutable state.

It moves mutable state into smaller, more explicit places:

  • tags
  • namespace roots
  • typed edges
  • leases
  • policies
  • transaction promotion

Those are exactly the places a registry should model deliberately.

A bucket stores bytes under keys.

A registry preserves an object graph, the mutable names around it, and the evidence required to move that graph between systems without losing its identity.