SDK — Publishing
Publishing a biome to the Xema Store is a controlled lifecycle transition, not a npm publish. Every transition is a capability call, every submission runs in the store-review execution environment before approval, and the published version is immutable.
This page covers the publisher-side flow: from a working biome in the org-installed lifecycle to a store-approved listing other orgs can find in the catalogue and install through biome lifecycle.
The four publishing transitions
| From → To | Capability | Who calls it |
|---|---|---|
org-installed → review-required | biome:promote@1 | Biome owner (org admin) |
review-required → store-submitted | biome:submit-to-store@1 (≡ store:biome.submit@1) | Biome owner |
store-submitted → store-approved | biome:approve-in-store@1 (≡ store:biome.approve@1) | Store reviewer |
store-approved → archived | biome:archive@1 (≡ store:biome.archive@1) | Biome owner or reviewer |
Every transition is mediated by xema-capability-router and stamped to audit-log-api. The biome owner cannot self-approve — biome:approve-in-store@1 requires a reviewer subject distinct from the submitting subject.
Step 1 — promote out of org-installed
When you are ready to start the publishing flow, promote the installation to review-required:
The xema CLI has no promote verb; invoke the biome:promote@1 capability with xema run, passing the installation as input:
1xema run biome:promote@1 --input @promote.jsonThe promotion call freezes the current draft into a published version (semver-tagged) and writes the manifest + bundle + permission digest into the review-required state. From this point the biome runs in sandbox + store-review and is no longer reachable from production org subjects.
Step 2 — submit to the Store
Once the review-required state holds the version you want to publish, submit:
1xema biome submit <installation-ref> --as <listing-id>Submission writes a StoreListingVersion row with lifecycle store-submitted via POST /listings/:id/versions on xema-store-api — a version is always addressed under its listing, never through a separate top-level submission route. The submission carries:
- the immutable manifest;
- the bundle, as a signed OCI artifact (see Bundle format below);
- the computed permission digest (capabilities + risk tier + data-access summary + diff against the previously approved version, if any);
- the publisher's signing identity.
From submission onward, the version runs only in store-review — no production data, no production credentials, no external network beyond what the environment allows.
Step 3 — review
The Store reviewer interacts with the submission through three endpoints on xema-store-api:
| Endpoint | Purpose |
|---|---|
POST /listings/:id/versions/:version/comment | Structured comment, recorded in the ReviewLog |
POST /listings/:id/versions/:version/approve | Flip to store-approved; the version becomes installable by any org |
POST /listings/:id/versions/:version/reject | Terminal rejection; the publisher must submit a new version |
GET /listings/:id/versions/:version/review-log is the audit trail. Comments are not free-form admin notes — every comment is structurally addressable so the publisher can read and respond.
Approval is the publisher's signal that the version is ready for general install. There is no "soft-launch" or "preview" intermediate state; the version is either store-submitted, store-approved, archived, or rejected.
Step 4 — installing
The Store does not install. Once approved, the version appears in the catalogue and an org installs it through biome lifecycle: the host plans the install, the admin reviews the plan (permission digest, dependencies), and the host applies exactly what was planned.
Bundle format — OCI artifacts
Biomes ship as OCI artifacts, not npm packages and not bare .tgz files. The same container-registry substrate that holds Docker images stores biome artifacts; the same cosign toolchain that signs container images signs biomes.
Layer layout — one layer per file
A biome bundle is a single OCI artifact whose artifactType is:
application/vnd.xema.biome.manifest.v1+json
Every file in the biome tree is pushed as its own layer. Each layer carries an org.opencontainers.image.title annotation holding that file's biome-root-relative POSIX path — xema-biome.json, skills/code-review/SKILL.md, api/acme-api/dist/main.js, and so on.
That annotation is load-bearing, not decoration. biome-fetcher-api unpacks a bundle by running oras pull --output <dir> <ref>, and oras materialises each layer at <dir>/<org.opencontainers.image.title> — the annotation is the only thing that reconstructs the directory tree. A layer without it is silently skipped by oras (Skipped pulling layers without file name in "org.opencontainers.image.title"), so an artifact built from opaque tarball layers pulls successfully, writes nothing, and installs an empty bundle. The fetcher then re-tars the reconstructed tree and requires a top-level xema-biome.json in it.
Two annotations are stamped on the artifact itself: org.opencontainers.image.title = the biome id, org.opencontainers.image.version = the manifest version.
What ships
Everything under the biome root except node_modules/ and .git/. There is no include-list and no per-directory layer split — manifest, source, compiled output, contributions, skills and assets all travel as ordinary files. Layer order is the sorted relative path, so the same tree produces the same layer sequence on every publish.
Registry authorization and repository namespaces
Pushing and pulling is authorized by the Xema container registry service, which implements the OCI Distribution token flow. It issues tokens and records an audit entry for each decision; it never carries image or artifact bytes. Your client talks to the registry directly, and the registry checks the token on every request.
- The client calls the registry and is told to get a token from the Xema token endpoint,
GET /v2/token, with the repository and actions it wants (pull,push,delete). - The token endpoint verifies your Xema access token and returns a signed bearer token valid for 5 minutes, scoped to the access it granted. A request that asks for nothing is a login probe and gets an empty-access token.
- The client presents the bearer token to the registry for each pull or push, and asks again after it expires.
The repository path decides who may use it:
| Repository path | Who can pull | Who can push |
|---|---|---|
<org-slug>/<name> | Callers whose organization is that org | Callers whose organization is that org |
system-shareable/<name> | Anyone, including anonymous callers | Xema platform only |
system/<name> | Xema platform only | Xema platform only |
Org repositories always have the org slug as their first segment; a single-segment name is rejected, so one org cannot reach another's namespace. The token endpoint returns exactly the actions it granted. It never silently trims a request: if nothing asked for is granted, the call is refused. When you publish to the Xema registry, put your org slug in the --registry value (for example <host>/<org-slug>), so the reference <registry>/<biome-id>:<tag> falls under your org namespace.
The same service also bakes custom workspace images for an org. An org admin creates a variant with a slug, a runtime kind, a base image and a manifest, and the service builds it asynchronously. A variant moves through draft, building, ready, failed and archived; re-creating an identical ready variant changes nothing, a failed or stale bake can be re-run, and archiving removes a variant from selection while keeping its history. One variant per runtime kind can be the org default. Baking and editing variants needs the workspace:image.bake@1 capability, listing needs workspace:image.use@1, and archiving needs workspace:image.publish@1.
Publishing tool
xema biome publish <path>, from the @xemahq/xema CLI. It resolves the target and the file set, runs oras push, then cosign sign — every prerequisite is fail-fast, nothing is skipped silently.
1xema biome publish ./ --registry ghcr.io/acme --token-env GHCR_TOKEN| Flag | Effect |
|---|---|
--registry <host> | Target OCI registry host, optionally with a namespace path. Overrides a configured oci source. |
--source <name> | Name of a configured oci source in xema.config.yaml to take the registry + token from. Required to disambiguate when several oci sources exist. |
--token-env <name> | Env-var NAME holding the registry token (used with --registry). |
--username <user> | Registry username paired with the token. Default: token. |
--tag <tag> | Tag to publish under. Default: the manifest version. |
--config <path> | Explicit xema.config.yaml path, instead of the standard search order. |
--dry-run | Print the resolved plan and the exact oras / cosign commands without touching the registry. |
The published reference is <registry>/<biome-id>:<tag>. The registry token is always referenced by env-var name — never inlined in config, never placed on the command line. The CLI resolves the name and hands the value to oras over --password-stdin. With no --token-env and no authTokenEnv on the configured oci source, oras uses ambient credentials; run oras login first.
Signing — keyed or keyless cosign
The publisher signs with a key (COSIGN_KEY) or keylessly (--keyless, Fulcio/OIDC from the CI's ambient token).
Keyless needs the run's own signer stated explicitly: --certificate-identity https://github.com/<job_workflow_ref> (the workflow file that actually runs, which is the reusable workflow when there is one) and --certificate-oidc-issuer https://token.actions.githubusercontent.com. Both flags are required with --keyless and refused without it.
A publish owes its own signer's signature and attestation. When the tag already holds the same content, the publisher checks with cosign verify / cosign verify-attestation against this run's signer: a signature or attestation by another identity (say, a develop-branch run before the main-branch run) does not count. Whatever is owed is added on the existing digest, without pushing again. In key mode that check needs COSIGN_KEY whenever the tag exists, dry runs included. Cosign must be logged in to the registry.
COSIGN_KEY holds the cosign private-key ref — the same env var the distribution tooling uses. It is read from the environment and never passed on argv. If COSIGN_KEY is unset, the publish fails before pushing anything; there is no unsigned mode and no override flag.
1COSIGN_KEY=cosign.key xema biome publish ./ --registry ghcr.io/acmeThe command run is cosign sign --yes --key <ref> <ociRef> — a registry signature on the pushed ref, which is what the fetcher's cosign verify looks for. A detached cosign sign-blob signature would not be found.
Verification is the broader of the two: BIOME_TRUSTED_PUBLISHERS accepts keyless certificate identities or a key:<path> public key, so an artifact signed keylessly by any CI pipeline, the Xema CLI's --keyless included, installs when its identity is allowlisted.
Provenance — SLSA v1.0
biome-fetcher-api requires a SLSA v1.0 in-toto attestation on the artifact by default: BIOME_REQUIRE_PROVENANCE defaults to "true". It is verified with cosign verify-attestation --type https://slsa.dev/provenance/v1 against the same trust anchors as the signature, and an artifact without one is refused with BIOME_PROVENANCE_MISSING / 403.
xema biome publish attaches it for you, as the third and final step of the publish — there is no separate command and no flag to skip it. It generates the predicate, writes it to a private temp file, and runs:
1cosign attest --yes --key "$COSIGN_KEY" \
2 --type https://slsa.dev/provenance/v1 \
3 ghcr.io/acme/<biome-id>:<tag> \
4 --predicate <temp>/slsa-provenance.jsonThe predicate is SLSA v1.0. buildDefinition.resolvedDependencies carries one entry per published file with its sha256 — one per pushed layer, so the attestation covers the complete build input rather than a subset. runDetails.builder.id is resolved in this order: XEMA_BIOME_BUILDER_ID if set, else the GitHub Actions GITHUB_WORKFLOW_REF, else local-developer://<user>@<host>. There is no unset case, so provenance is never silently omitted.
A deployment that sets BIOME_REQUIRE_PROVENANCE=false accepts a signature-only artifact — the signature is still mandatory.
Install-time enforcement
biome-fetcher-api verifies signature + provenance before any bytes are unpacked. Failure modes:
| Verdict | Error code | HTTP status |
|---|---|---|
| Signature missing | BIOME_SIGNATURE_MISSING | 403 |
| Signature invalid | BIOME_SIGNATURE_INVALID | 403 |
Identity not in BIOME_TRUSTED_PUBLISHERS | BIOME_SIGNATURE_IDENTITY_NOT_TRUSTED | 403 |
| SLSA provenance missing | BIOME_PROVENANCE_MISSING | 403 |
| SLSA provenance invalid | BIOME_PROVENANCE_INVALID | 403 |
cosign binary absent in container | BIOME_COSIGN_UNAVAILABLE | 500 |
See the biome supply chain page for the trusted-publisher allowlist runbook and the fetcher's trust configuration.
What does not change a published version
A published version is immutable. The following actions do not edit the version:
- Comment on a submission (writes a
ReviewLogrow, not a manifest change). - Update the listing's branding metadata (
PATCH /listings/:idupdates listing-level fields, not version-level). - Archive the listing (hides from new installs but keeps existing installs functional; their lockfile pins the version).
To ship a fix, publish a new version. The user-controlled versioning model is the same one that governs every other Xema object — there is no "edit in place" mode for published biomes.
Lockfile interaction
When a version is approved, xema-store-api derives a canonical lockfile entry for the bundle. Org-side installs reuse the canonical lockfile fragment in their own composite lockfile, so a biome that is store-approved at version 1.4.2 resolves to byte-identical pinned dependencies in every org that installs it.
This is what makes "the same biome in two orgs" mean exactly the same thing — versioning + lockfile + capability digest.
Related pages
- Store — listing and review endpoints
- Biomes — lifecycle — the underlying state machine
- Versioning — draft vs published vs lockfile
- Manifest reference — what the submission carries
- Testing — what reviewers run against a submission
Previous: ← Storage Next: UI I contribute →