Appearance
Version storage
A Matrix package version is identified by (packageName, version). The artifact registry stores tarballs keyed by that pair, and the package catalog indexes metadata by it.
Two parallel stores
There are two things stored per version:
| What | Where | Format |
|---|---|---|
| Tarball bytes | Gitea blob storage | package-version.tgz |
| Discovery metadata | discovery-index.json next to the tarballs | JSON entry |
These are written together by the publish pipeline but read independently:
npm installreads only tarballs.system.packages.registry.searchreads onlydiscovery-index.json.
Tarball storage
Gitea owns the blob layer. Tarball lookup is HTTP:
GET /api/packages/<owner>/npm/<pkg>/-/<pkg>-<version>.tgzInternally Gitea stores the tarball content-addressed under its <gitea-data>/packages/ directory. That layout is Gitea's concern, not Matrix's; Matrix only assumes the public URL surface.
Discovery-index.json shape
The companion metadata index lives at the registry root (next to the tarball storage). Its shape is:
json
{
"packages": {
"@open-matrix/chat": {
"versions": {
"0.2.3": {
"indexedAt": "2026-04-30T12:00:00Z",
"indexedBy": "publish-pipeline",
"metadata": {
"packageName": "@open-matrix/chat",
"version": "0.2.3",
"displayName": "Chat",
"description": "...",
"inferredKind": "webapp",
"tags": [...],
"capabilities": [...],
"webapp": { "appName": "chat", "shells": ["platform"], ... },
"components": [
{
"type": "ChatActor",
"mount": "chat",
"accepts": ["chat.send", "chat.thread.list"],
"emits": ["chat.message"],
...
}
]
}
},
"0.2.4": { ... }
}
},
"@open-matrix/director": { ... }
}
}The exact field shape is the IPackageDiscoveryIndex interface in projects/matrix-3/packages/mx-cli/src/utils/discovery-index.ts. The reader in SystemPackageRegistryActor.readLatestMetadata extracts the latest-version entry per package by sorting version keys natural-numerically:
typescript
// SystemPackageRegistryActor.ts:218-228 (paraphrased)
const latest = Object.entries(versionsRecord)
.sort(([left], [right]) => left.localeCompare(right, undefined, { numeric: true }))
.at(-1)?.[1];Version sort semantics
localeCompare(..., { numeric: true }) is not semver-aware. It does right-thing comparisons on 0.2.3 vs 0.2.10 (treating numbers numerically), but it does not know that 0.2.3-rc.1 < 0.2.3. For pre-release ordering, use proper semver tooling at publish time (the publish pipeline uses semver.compare).
Note: The catalog's "latest" view is a coarse approximation. For pre-release-aware version selection, consumers should run install resolution with semver, not rely on the catalog's "latest" projection.
Storage durability
| Concern | Backend | Notes |
|---|---|---|
| Tarball bytes | Gitea volume | Survives container restarts; lost if volume is destroyed. Backups are operational concern. |
| Discovery index | filesystem | Same volume as tarballs. Re-buildable by re-reading every tarball's matrix.json if lost. |
There is no replication today. The registry is single-instance per deployment. A future revision could add S3/blob-store backing for tarballs; the catalog index can be regenerated from tarball contents.
Yanking / deletion
Gitea allows package deletion via its admin UI / API. Matrix's own tooling does not currently include a mx unpublish command. Operators can:
- Delete the version from Gitea.
- Re-run the publish pipeline's index step against the post-deletion directory to update
discovery-index.json.
Status: No automated yank flow exists. This is an open operator action.
See also
Source:
projects/matrix-3/packages/system-catalog/src/SystemPackageRegistryActor.ts:211-230,projects/matrix-3/packages/mx-cli/src/utils/discovery-index.ts.