An RDF-native, self-hostable, federatable registry of software, the deployments that run it, the runs they perform, and the data artifacts those runs consume and produce.
📖 Documentation — the model,
the API, authentication, the vocabulary rules, deployment. The chapters live in docs/
and read on GitHub as they are; pushing to the default branch publishes the site.
Those four things are usually kept in four systems that do not agree. A software catalogue knows what exists but not where it runs; a monitoring system knows what is running but not what it is for; a data catalogue knows a file exists but not which program wrote it. None of them can answer a question that crosses two.
Putting them in one graph buys two things:
- Matchmaking, before anything has run. A deployment declares what it is able to produce and consume, so "what here could produce this kind of artifact?" is answerable on an empty registry — which is the question you have when you are looking for a tool rather than a file.
- Lineage, after it has. A run links what it used to what it generated, so "where did this come from and who else used it?" is a graph walk.
It holds descriptions and pointers, never bytes.
cargo build --release
cd frontend && npm install && npm run build && cd ..
export TAR_BASE_IRI=http://127.0.0.1:8080
export TAR_ROOT_TOKEN=$(openssl rand -hex 24)
export TAR_DATA_DIR=./data
./target/release/tar seed # example content, so a fresh install is not an empty page
./target/release/tar serveOpen http://127.0.0.1:8080. Everything reads anonymously; sign in with the root token to register or edit.
With Docker:
export TAR_ROOT_TOKEN=$(openssl rand -hex 24)
docker compose up --buildOr the published image, which is what a real deployment runs:
docker run -d -p 8080:8080 \
-e TAR_BASE_IRI=https://registry.example.org \
-e TAR_ROOT_TOKEN="$TAR_ROOT_TOKEN" \
-v tar-data:/data \
ghcr.io/maastrichtu-ids/tool-artifact-registry:0.1.0compose.identity.yaml brings up the registry and an identity provider with its realm already
imported, in one command. Deployment covers a single container,
compose, and Kubernetes manifests under deploy/kubernetes/.
TAR_BASE_IRI is the only universally required setting — the registry cannot mint
dereferenceable identifiers without knowing what it is called. Everything else has a working
default. tar config prints the effective configuration with secrets redacted.
See Getting started for the rest.
Five commitments explain most of the design. Each is argued properly in the documentation.
Writes are validated by real SHACL. shapes/tar-shapes.ttl is the rule set, enforced before
anything is committed. Changing what the API accepts is an edit to a Turtle file, not to Rust. A
rejected write returns 422 with the engine's own sh:ValidationReport, plus a tar:jsonField
per result so a form can attach the error to the input that caused it.
FAIR is not open. An artifact can be recorded as findable, described, and provably not
retrievable: no download URL exists at all, the UI renders no download affordance, and the
Signposting headers omit rel="item" — so a machine can tell "no bytes here" from "bytes behind
auth" without parsing the body and guessing.
Vocabulary is checked, not suggested. An artifact type must be a term the registry actually holds. Free-text classification degrades silently: three callers spell the same thing three ways, a filter finds a third of what is there, and a subscription written against one spelling never fires — which is indistinguishable from a subscription with nothing to deliver. A write naming an unknown term is refused before anything is written, and the refusal says how to search for the right one, adopt an existing one, or mint a new one.
Federation is a cross-link, not a harvest. Any object position may hold a foreign IRI. Advertising never blocks on the network: an unknown IRI is stored verbatim and a background worker fetches a stub into that peer's own named graph, never mixed with local records.
Every identifier dereferences. A record's IRI is also its web page, its Turtle, its JSON-LD and its Markdown — the same graph through one code path, so the prose cannot drift from the RDF.
curl -H 'Accept: text/turtle' localhost:8080/software/01a05…
curl -H 'Accept: application/json' localhost:8080/software/01a05…
curl localhost:8080/software/01a05….mdA working prototype. Every endpoint in the design is implemented and covered by tests. Where it departs from the design, or stops short of it, that is written down in Limitations rather than left to be discovered.
src/
api/ HTTP surface: routes, dereference, SPARQL, SPA serving
auth/ principals, roles, scopes, and JWT/JWKS workload identity
domain/ projections between the graph and the JSON API
mcp/ the hosted Model Context Protocol server
rdf/ property maps and quad builders
store/ GraphStore trait, portable SPARQL, embedded Oxigraph and remote-endpoint backends
ops/ SQLite: tokens, peers, audit, federation, subscriptions, idempotency
health.rs background liveness probing of deployment endpoints
shacl.rs write validation and sh:ValidationReport generation
negotiate.rs content negotiation and FAIR Signposting
llms.rs the llms.txt index
seed.rs example content, and the boot-time graph migrations
bundles.rs the bundled reference data: an in-memory store, and a hash-guarded copy
in the record store, one named graph per bundle
shapes/ SHACL shapes and the bundled vocabularies
frontend/ React 18 + Vite + TypeScript UI
docs/ the documentation site (mdBook)
deploy/ Kubernetes manifests, and a local identity provider with an importable realm
tests/ end-to-end tests against the real router
cargo test # unit, end-to-end, MCP and subscription suites
cd frontend && npm test # component, parsing and screen testsBoth run on every push and pull request (.github/workflows/ci.yml). Neither needs the network
or a service.
The graph store is a trait with two implementations — embedded Oxigraph, the default, and any external SPARQL 1.1 endpoint (Graph store). The same end-to-end suite runs against either, so the two answering identically is enforced rather than asserted. Point it at a real server and it creates a dataset per test:
docker run --rm -d --name tar-fuseki -p 3030:3030 -e ADMIN_PASSWORD=admin stain/jena-fuseki
TAR_TEST_SPARQL_ENDPOINT=http://127.0.0.1:3030 cargo testEverything else lives in docs/, which builds into a site with
mdBook:
cargo install mdbook --locked
mdbook serve docs --open| The model | Software → releases → deployments → runs → artifacts, and capabilities. |
| Getting started | Running it, seeding it, first requests. |
| API | Organised by task: registering, advertising, searching, subscribing, federating. |
| How a tool authenticates | The three credential types and when each is right. |
| Vocabulary | What types and topics must be, and how to search, adopt or mint one. |
The tar: ontology |
The classes and properties the registry declares, and why nothing standard fitted. |
| For agents | llms.txt, Markdown representations, and the hosted MCP server. |
| Deploying it | A container, compose, or a cluster — and the one setting you cannot change later. |
| Operating a registry | Configuration, identity provider, backup. |
| Limitations | The honest list. |
| Design record | What was decided, and what else was considered. |
Apache-2.0.