Registry Internals

The registry stores versioned entry state, supports transactions and history, and propagates changes through the event bus.

The Go and query fragments on this page document internal data structures and finder syntax; they are not standalone application examples.

Entry Storage

Entries are stored as an ordered slice with a hash map index for O(1) lookups:

type Entry struct {
    ID       ID              // namespace:name
    Kind     Kind            // Entry type
    Meta     attrs.Bag       // Author metadata
    Data     payload.Payload // Content
    Registry EntryMetadata   // Registry-owned provenance
}

type EntryMetadata struct {
    Owner string // Deployment source that supplied the entry
    Root  bool   // Dependency declaration selected by the deployment
}

Entry IDs use Go's unique package for interning—identical IDs share memory.

Registry is owned by the registry, not the entry author. Owner is assigned from the deployment source; Root is set from the dependency_root write-side field on an ns.dependency entry. The ordinary entry APIs return only ID, Kind, Meta and Data; provenance is read through the snapshot state API.

Snapshot

Registry.Snapshot() returns one atomic view: the version, the entries at that version, and the registry-owned state metadata for that same version.

type Snapshot struct {
    Registry StateMetadata
    Version  Version
    Entries  State
}

type StateMetadata struct {
    Resolution *DependencyResolution
}

Reading version, entries and resolution as one value prevents a caller from pairing entries with a resolution from a different version. The selected module graph is stored once per snapshot rather than repeated on every entry.

Overlays

OverlayWriter is an optional registry capability for process-local entries:

type OverlayWriter interface {
    ApplyOverlay(context.Context, string, uint64, ChangeSet) (uint64, error)
    GetOverlay(string) (State, uint64, error)
}

Overlay entries are grouped under a logical owner string. They join effective state and pass through the same topology sort and handler transitions as durable entries, so services start and stop for them normally, but they never produce a history version. They are empty after a cold boot and must be reconciled by their owning control service.

Writes are optimistically concurrent: GetOverlay returns the owner's current generation, and ApplyOverlay commits only if that generation is still current, otherwise it returns a retryable Conflict. Each successful apply issues a new process-unique generation, and a tombstone is retained for owners that mutated so an ABA sequence cannot be mistaken for an unchanged overlay.

The composition rules validated on every apply:

  • An entry may be created only if no durable entry and no overlay entry holds its ID.
  • Only the owning identity may update or delete its overlay entries.
  • Overlay entries may not carry registry-owned metadata, and may not use kinds claimed by registry directives.
  • A delete may not remove an entry that a surviving entry depends on.
  • Dependency edges may not cross owner boundaries, and durable entries may not depend on overlay entries.

Version Chain

Each version points to its parent. Path computation uses a graph algorithm to find the shortest route between any two versions:

flowchart LR
    v0[v0] --> v1[v1] --> v2[v2] --> v3[v3] --> vN[vN]

ChangeSets

A changeset is an ordered list of operations transforming one state to another:

Operation OriginalEntry Purpose
Create nil Add new entry
Update old value Modify existing
Delete deleted value Remove entry

OriginalEntry enables reversal—updates store the previous value, deletes store what was removed.

Building Deltas

BuildDelta(oldState, newState) generates minimal operations:

  1. Compare states, identify changes
  2. Sort deletes in reverse dependency order (dependents first)
  3. Sort creates/updates in forward dependency order (dependencies first)

Squashing

Multiple changesets merge by tracking final state per entry:

Create + Update = Create (with updated value)
Create + Delete = ∅ (cancel out)
Update + Delete = Delete
Delete + Create = Update

Transactions

sequenceDiagram
    participant R as Registry
    participant B as EventBus
    participant H as Handlers

    R->>B: registry.begin
    loop Each Operation
        R->>B: entry.create/update/delete
        B->>H: dispatch to listeners
        H-->>B: accept or reject
        B-->>R: confirmation
    end
    alt All accepted
        R->>B: registry.commit
    else Any rejected
        R->>B: registry.discard
        R->>R: rollback
    end

By default, the registry waits 30 seconds for listeners to accept or reject each operation. registry.event_wait_timeout changes this per-operation timeout. On rejection, the registry rolls back by computing and applying the inverse delta.

Non-propagating Entries

The following kinds skip the event bus by default:

  • registry.entry - Application configs
  • ns.requirement - Namespace requirements
  • ns.dependency - Module dependencies
  • ns.definition - Module metadata (readme, wiki, license, authors)

This is the default set; registry.dispatch_internal_kinds in the runtime config replaces it.

Dependency Resolution

Entries can declare dependencies on other entries. The resolver extracts dependencies via registered patterns:

resolver.RegisterPattern(registry.DependencyPattern{
    Path:          "meta.server",
    AllowWildcard: true,
})

Dependencies are extracted from entry Meta and Data fields, then used for topological sorting during state transitions.

Dependency Access Policy

External dependency access is a request-scoped context value, not a global flag:

Policy Effect
DependencyAccessUnspecified Callers choose; the caller's own default applies
DependencyAccessOnline External resolution and artifact download are permitted
DependencyAccessVerifiedOffline External access is forbidden; resolution uses locked manifests and locally present artifacts

LoadState() defaults to verified-offline when the context specifies nothing, so boot replays a stored graph without reaching the network. Restoring a deployment baseline switches the context to online because it must fetch the modules that baseline names. Under verified-offline a manifest provider serving only locked modules replaces the hub provider, and a missing artifact fails as missing evidence rather than triggering a download.

Version History

History backends:

Implementation Use Case
SQLite Production persistence
PostgreSQL Production persistence, shared across nodes
Memory Default when history_type is unset; testing
Nil No history

SQLite uses WAL mode with tables for versions, changesets (MessagePack encoded), and metadata. PostgreSQL is selected with registry.history_type: postgres plus history_dsn/history_schema (see Configuration).

History also persists the exact dependency resolution for each version: when an ns.dependency change is applied, the resolved module graph is stored content-addressed alongside the changeset. Boot and rollback replay the stored graph instead of re-solving, so a version always reconciles with the versions it was resolved with. The history schema migrates automatically on first boot after an upgrade; a pre-existing version is resolved once on first visit and checkpointed.

Path computation finds the shortest route between versions:

Path(v0, v3) = [v1, v2, v3]  // Apply changesets forward
Path(v3, v1) = [v2, v1]      // Apply reversed changesets

LoadState() replays history from a baseline without creating new versions—used during boot.

Finder

Query engine with LRU caching for searching entries:

Operator Prefix Example
Root-field glob . root field .kind=function.*
Regex ~ ~meta.path=/api/.*
Contains * *meta.tags=backend
Prefix ^ ^meta.name=user
Suffix $ $meta.path=Handler

Cache invalidates on version change.

Glob matching applies to the root fields .kind, .name, .ns, and .id. Unprefixed meta.* criteria use equality matching.

See Also