Artifacts & Model Registry¶
Every materialized result in Strata is an immutable, versioned artifact with content-addressed provenance. The registry layer adds the pointers and history that turn the artifact store into a lightweight model registry: names, aliases, tags, an append-only audit, and optional approval gates — all in the same SQLite metadata store, no extra services.
Names¶
A name is a mutable, human-readable pointer to one artifact version:
client.materialize(inputs=[...], transform=..., name="taxi/tip-model")
client.set_name("taxi/tip-model", artifact_id, version)
resolved = client.resolve_name("taxi/tip-model")
Slash-namespaced names (team/dataset/raw) are the natural convention and
fully supported. A name tracks "the latest blessed build of this line";
every move is audited.
Aliases¶
Aliases are intent pointers on a name — the post-stages registry model (champion/candidate rather than Staging/Production enums):
client.set_alias("taxi/tip-model", "champion", artifact_id, version)
client.set_alias("taxi/tip-model", "candidate", new_id, new_version)
client.resolve_alias("taxi/tip-model", "champion")
A name can hold any number of aliases. Aliases may pin a superseded version (an old champion stays fetchable after a rebuild), and an aliased artifact is protected from garbage collection. Setting an alias to the version it already points at is a no-op — idempotent promote cells re-run without spamming history.
In the artifact CLI, name@alias works anywhere a reference does:
strata artifact show taxi/tip-model@champion
strata artifact lineage taxi/tip-model@champion
strata artifact pull taxi/tip-model@champion --to model.arrow
Tags¶
Key/value facts about one artifact version — recorded at promote time, queryable later:
client.set_tag(artifact_id, version, "mae", "1.226")
client.set_tag(artifact_id, version, "trained_at_snapshot", str(snapshot))
client.get_tags(artifact_id, version)
Audit¶
Every name, alias, and tag mutation lands in an append-only audit table — written in the same transaction as the mutation, so a change can never land unrecorded. The audit answers "what did champion point to before?":
$ strata artifact audit taxi/tip-model
2026-06-07 06:01 alias_set taxi/tip-model@champion 4861a747@v1 -> e3ea60b6@v1
2026-06-07 06:01 alias_approved taxi/tip-model@champion
2026-06-07 06:01 alias_request_set taxi/tip-model@champion
Entries carry the actor (principal id when authenticated), the action, and
the from → to versions. GET /v1/registry/audit serves the same data; the
SDK exposes get_registry_audit(name=..., artifact_id=...).
Approval gates¶
Protect aliases whose moves should require a human:
Moves (and deletes) of protected aliases return 202 pending instead of applying. The queue is visible, and approval applies the move with the approver as the audit actor — atomically with the pending-consumption, so a crash can never swallow an approval:
client.list_pending_changes()
client.approve_alias_change("taxi/tip-model", "champion")
client.reject_alias_change("taxi/tip-model", "champion")
If a pending change's target artifact disappears before approval, the approve fails cleanly and the pending entry stays for an explicit reject. Unprotected aliases apply immediately; the default is no gating.
Authorization (service mode). When exposed under trusted-proxy auth,
approve and reject require the admin:registry scope, and approval
enforces separation of duty — the requester cannot self-approve their own
move unless they hold the admin:* break-glass scope. The audit log is
tenant-scoped: a principal reads only its own tenant's history (admin:*
sees the whole store). In personal mode (single operator, no auth) these
gates are inert. Resolving names/aliases is a tenant-scoped read in service
mode; publishing them (set_name / set_alias) requires opting into
service_writes_enabled and holding the artifacts:write scope — see
Service Mode → shared research store.
The promotion flow, end to end¶
The shape this is designed for — a notebook training pipeline whose last cell promotes through the registry:
# promote cell
model_art = client.put(inputs=[features_uri], transform=..., name="taxi/tip-model")
client.set_tag(model_art.artifact_id, model_art.version, "mae", f"{mae:.4f}")
move = client.set_alias("taxi/tip-model", "champion",
model_art.artifact_id, model_art.version)
print(move.get("status", "applied")) # "applied" | "pending" | "unchanged"
New data lands in the lake → the @table annotation
makes the training cells stale → the pipeline re-runs → promote proposes the
challenger → a human approves → and the whole history is reconstructible:
strata artifact lineage taxi/tip-model@champion
# model <- features <- scan <- table file://...#nyc.trips @ snapshot 2558063...
strata artifact audit taxi/tip-model
strata artifact verify # store-wide blob/metadata consistency check
In the notebook (the registry dashboard)¶
Everything above also has a UI — the registry is a first-class surface in the notebook, so promotion and approvals don't have to be code. Registry routes are personal-mode only today, so the dashboard hides itself in service mode.
New to it? Start with the walkthrough
For a click-by-click guide — publish from a cell → promote → approve → lineage — see Registry Dashboard. The rest of this section is the reference for what each piece does.
Publishing. Inside a cell, the ambient strata
client publishes with a name:
The artifact lands in the registry and is stamped with the cell that produced it, so it shows up right under that cell.
Per-cell strip. Below a cell that published, a compact strip shows what it
put into the registry — ⬡ taxi/tip-model v3 ★champion mae=1.98 — with a
[Promote▾] menu (champion / candidate) and a ⎘ lineage button. Promote
where you trained the model, without leaving the cell.
Registry tab. The bottom drawer has a Registry tab:
- a pending-approval banner with Approve / Reject — the human gate, in the UI (a protected-alias move queues here);
- a names table — each name with its alias chips (
★champ,cand), latest version, tags, and[Promote▾]; - a collapsible audit timeline.
Promote feedback. Setting an alias toasts the result: ✓ taxi/tip-model →
champion when it applies, or ⏳ champion change pending approval when the
alias is protected — at which point the pending banner appears for someone to
approve. Unprotected aliases (candidate) apply immediately.
Lineage. The ⎘ button (strip or names table) opens the provenance chain —
model ← features ← scan ← table @ snapshot — the same lineage the CLI prints,
as an interactive view.
So the same promote → gate → approve → audit loop the SDK/CLI drives is a few clicks in the notebook, backed by the identical audited routes.
Storage & durability¶
Registry state lives in the same artifacts.sqlite as artifact metadata
(WAL mode, transaction-per-mutation). Everything commits before the API
responds; the audit is in-transaction with its mutation; server restarts
are non-events. The CLI reads the store directly — server up or down.