Use when discovering or registering Solid data by RDF class, reading publicTypeIndex or privateTypeIndex links, creating missing Type Index documents, or implementing TypeRegistration wrappers and bounded pod search.
Install
npx skillscat add jeswr/solid-sdk/solid-type-index Install via the SkillsCat registry.
Quick reference
Use the Solid Type Index
The Type Index advertises where data of a given RDF class lives. It is a discovery hint, not an authorization grant.
Read
- Fetch the WebID profile for
solid:publicTypeIndex; follow itspim:preferencesFilelink and readsolid:privateTypeIndexfrom the Preferences Document (with the WebID as subject). - Fetch the index and find
solid:TypeRegistrationsubjects whosesolid:forClassmatches the desired class. - Read either
solid:instancefor one resource orsolid:instanceContainerfor a collection. - Fetch the discovered location and apply its real access controls.
Register
- Create a
TypeRegistrationwith exactly one ofsolid:instanceorsolid:instanceContainer. - Use typed
DatasetWrapper/TermWrapperclasses and RDF/JS terms; do not concatenate Turtle or hand-add quads. - Serialize the full dataset with
n3.Writerand use conditionalPUTwith explicit Turtle content type. - On
412, re-fetch, reapply the registration, and retry according to the caller's bounded conflict policy.
Bootstrap missing indexes
Do not assume the server seeded them. When an index is genuinely absent, create a solid:TypeIndex plus solid:ListedDocument (public) or solid:UnlistedDocument (private). Link the public index from the WebID Profile Document and the private index from the Preferences Document, discovering or creating the latter via pim:preferencesFile. Use conditional writes. Never overwrite an index or preferences document that was merely unreadable.
Keep the profile publicly readable and protect the private index itself through the effective access-control system.
Search and security
- Validate every profile-linked index and registered location against the user's advertised storage roots before attaching credentials.
- Use parsed origin plus path-segment containment, not raw string prefix.
- Bound client-side pod search by source count, result count, and wall-clock time, including the discovery phase.
- De-duplicate results by resource URL and label capped results as incomplete.
- Scheme-filter every RDF-derived URL before rendering it as a link or image.
When interoperating with a deployed application, verify the actual predicates its shipping code reads and writes; a vocabulary document alone may not reflect de-facto data.
Agent persona
When this work is delegated to a sub-agent, spawn the solid-data-modeler persona from
`.claude/agents/solid-data-modeler.md` — it routes through this skill.
Orchestration: .claude/agents/solid-app-orchestration.md.
Solid Type Index
The Type Index is the convention by which a pod owner advertises where in the
pod a given RDF class is stored, so independent apps can discover each other's
data instead of hard-coding paths. Two indexes per profile: a public one
(discoverable types) and a private one (sensitive types).
This skill resolves the deferral in AGENTS.md §Writing data ("the type index
is the discovery mechanism for other apps' data; no typed wrapper ships for it
yet… out of scope"). No @solid/object wrapper ships for it, so you implement
read+write through your own TermWrapper subclasses — never inline quads, never
string-concatenated Turtle. Follow the house stack in AGENTS.md: auth via@solid/reactive-authentication (plain fetch), I/O via @jeswr/fetch-rdf,
serialisation via n3.
Stay in scope. Use the Type Index for interop — locating/exposing data
other apps share. For data only your own app touches, derive paths from the
pod root (AGENTS.md§Writing data); you do not need a registration.
Vocabulary
The solid: namespace is http://www.w3.org/ns/solid/terms#
(type-indexes spec). Predicate/class
IRIs (verified against the spec):
| Term | IRI suffix on solid: |
Role |
|---|---|---|
solid:publicTypeIndex |
publicTypeIndex |
profile → public index doc |
solid:privateTypeIndex |
privateTypeIndex |
Preferences Document (WebID subject) → private index doc |
solid:TypeIndex |
TypeIndex |
rdf:type of an index document |
solid:ListedDocument |
ListedDocument |
extra rdf:type on a public index |
solid:UnlistedDocument |
UnlistedDocument |
extra rdf:type on a private index |
solid:TypeRegistration |
TypeRegistration |
rdf:type of one registration entry |
solid:forClass |
forClass |
registration → the RDF class it indexes (IRI) |
solid:instance |
instance |
registration → a single resource (IRI) |
solid:instanceContainer |
instanceContainer |
registration → a container to list (IRI) |
rdf:type is http://www.w3.org/1999/02/22-rdf-syntax-ns#type. A registration
uses solid:instance (one resource) or solid:instanceContainer (a
container of many) — not both.
Read path: discover an index, then a class
- Fetch the WebID profile (
AGENTS.md§Reading data); readsolid:publicTypeIndex
andpim:preferencesFileoff the WebID subject. Fetch the linked Preferences Document
with auth and readsolid:privateTypeIndexoff the same WebID subject there. - Fetch the index document; find
solid:TypeRegistrationsubjects whosesolid:forClassmatches the class you want; readsolid:instance/solid:instanceContainer. - Discovery is a hint, not a grant. A registration tells you where data
might be — you must still GET the resource to learn your actual access
(forum).
Reading the registry can also be done with a SPARQL query that pairssolid:forClass with solid:instance/solid:instanceContainer
(forum);
the wrapper approach below is the house default.
Convention, not enforcement — always fallback-and-create
The server does not maintain the index. It is convention-only: every app
must cooperate, and an app cannot assume another app registered correctly
(forum).
The Community Solid Server does not seed type-index files. Tim Berners-Lee:
apps/providers must create them, in "the same place they are now"
(forum).
NSS auto-seeds; CSS does not. So never assume the indexes exist — read,
and if absent, create-and-link.
When creating a preferences/profile file to host the private index, do not
clobber an existing one you merely failed to read (e.g. on a 403). Treat
unreadable as "may exist", not "absent" (TBL, same thread).
Create-and-link recipe (when an index is absent)
- Read the profile and, for a private index, discover
pim:preferencesFile
(http://www.w3.org/ns/pim/space#preferencesFile) on the WebID subject. Validate that
URL against the user's advertised storage roots before an authenticated fetch.
If no preferences link exists, choose<podRoot>settings/prefs.ttland read it first.
Reuse an existing document; only a confirmed404permits creating one withIf-None-Match: *. A401,403, network error, or parse error is not absence. - Choose a location near the profile — convention:
<podRoot>settings/publicTypeIndex.ttl
and<podRoot>settings/privateTypeIndex.ttl(the spec's examples use/settings/). - PUT the confirmed-missing index document with
If-None-Match: *, typedsolid:TypeIndexandsolid:ListedDocument(public) /solid:UnlistedDocument
(private). Only bootstrap with Write and Control access to the root storage. - For a public index, add
solid:publicTypeIndexon the WebID subject in the
WebID Profile Document. For a private index, addsolid:privateTypeIndex
on the WebID subject in the Preferences Document. Persist the full modified
dataset withIf-Matchusing that document's ETag; create genuinely absent documents
withIf-None-Match: *. On412, re-fetch and reconcile without dropping other data;
stop if a safe conditional update is unavailable. - If the preferences link was missing, conditionally update the profile to add
<webId> pim:preferencesFile <preferencesUrl>only after the preferences document
has been persisted. Keep the WebID card world-readable; restrict preferences and the
private index to the owner through the effective access-control system.
The bootstrap specification
requires the public pointer in the profile and the private pointer in preferences. For example,
the profile at https://pod.example/profile/card contains:
@prefix solid: <http://www.w3.org/ns/solid/terms#> .
@prefix pim: <http://www.w3.org/ns/pim/space#> .
<#me> solid:publicTypeIndex </settings/publicTypeIndex.ttl> ;
pim:preferencesFile </settings/prefs.ttl> .The Preferences Document at https://pod.example/settings/prefs.ttl contains:
@prefix solid: <http://www.w3.org/ns/solid/terms#> .
<https://pod.example/profile/card#me> solid:privateTypeIndex </settings/privateTypeIndex.ttl> .Spec-exact shapes (type-indexes):
# public index document
<> a solid:TypeIndex, solid:ListedDocument .
<#registration-ab09fd> a solid:TypeRegistration ;
solid:forClass vcard:AddressBook ;
solid:instance </public/contacts/myPublicAddressBook.ttl> .
# private index document
<> a solid:TypeIndex, solid:UnlistedDocument .
<#registration-ab09fd> a solid:TypeRegistration ;
solid:forClass vcard:AddressBook ;
solid:instanceContainer </private/myBookmarks/> .Worked TermWrapper implementation
Two wrappers: TypeIndexDataset (the whole index document — a DatasetWrapper,
because registrations are sibling subjects in the document, not objects
reachable from the document's own subject) and TypeRegistration (one entry —
a TermWrapper). All forClass/instance/instanceContainer values are
IRIs, so use the NamedNode* mappers; rdf:type is a set, so use SetFrom.
Compile-verified (tsc --strict) against the published @rdfjs/wrapper
0.34.0 + @jeswr/fetch-rdf 0.1.0.
import {
TermWrapper, DatasetWrapper,
OptionalFrom, OptionalAs,
SetFrom,
NamedNodeAs, NamedNodeFrom,
} from "@rdfjs/wrapper";
const SOLID = "http://www.w3.org/ns/solid/terms#";
const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
/** One `solid:TypeRegistration` entry. */
class TypeRegistration extends TermWrapper {
/** The RDF class this entry indexes (an IRI). */
get forClass(): string | undefined {
return OptionalFrom.subjectPredicate(this, SOLID + "forClass", NamedNodeAs.string);
}
set forClass(v: string | undefined) {
OptionalAs.object(this, SOLID + "forClass", v, NamedNodeFrom.string);
}
/** A single resource holding instances of `forClass`. */
get instance(): string | undefined {
return OptionalFrom.subjectPredicate(this, SOLID + "instance", NamedNodeAs.string);
}
set instance(v: string | undefined) {
OptionalAs.object(this, SOLID + "instance", v, NamedNodeFrom.string);
}
/** A container listing instances of `forClass`. */
get instanceContainer(): string | undefined {
return OptionalFrom.subjectPredicate(this, SOLID + "instanceContainer", NamedNodeAs.string);
}
set instanceContainer(v: string | undefined) {
OptionalAs.object(this, SOLID + "instanceContainer", v, NamedNodeFrom.string);
}
/** Stamp the entry as a TypeRegistration (call once when minting). */
markRegistration(): void {
this.types.add(SOLID + "TypeRegistration");
}
/** Live set of `rdf:type` IRIs. */
get types(): Set<string> {
return SetFrom.subjectPredicate(this, RDF + "type", NamedNodeAs.string, NamedNodeFrom.string);
}
}
/** A type-index document, wrapped whole. */
class TypeIndexDataset extends DatasetWrapper {
/** Every `solid:TypeRegistration` subject in the document. */
get registrations(): Iterable<TypeRegistration> {
return this.instancesOf(SOLID + "TypeRegistration", TypeRegistration);
}
/** Find the location(s) registered for a class IRI. */
locate(classIri: string): { instance?: string; container?: string }[] {
const out: { instance?: string; container?: string }[] = [];
for (const reg of this.registrations) {
if (reg.forClass === classIri) {
out.push({ instance: reg.instance, container: reg.instanceContainer });
}
}
return out;
}
/** Add a registration; serialise + conditional PUT afterwards to persist. */
register(
indexUrl: string, // the type-index document URL
fragment: string, // e.g. "#registration-notes"
classIri: string,
location: { instance?: string; container?: string },
): TypeRegistration {
// `this` IS a DatasetCore (DatasetWrapper implements it), so the new
// wrapper writes into the same underlying dataset.
const reg = new TypeRegistration(indexUrl + fragment, this, this.factory);
reg.markRegistration();
reg.forClass = classIri;
if (location.instance) reg.instance = location.instance;
if (location.container) reg.instanceContainer = location.container;
return reg;
}
}
// usage
import { fetchRdf } from "@jeswr/fetch-rdf";
import { DataFactory } from "n3";
const { dataset, etag } = await fetchRdf(indexUrl);
const index = new TypeIndexDataset(dataset, DataFactory);
const where = index.locate("https://schema.org/TextDigitalDocument");
index.register(indexUrl, "#registration-notes", "https://schema.org/TextDigitalDocument", {
container: new URL("notes/", podRoot).toString(),
});Then mint + persist exactly as AGENTS.md §Writing data prescribes: mutate the
in-memory dataset through these wrappers, serialise the whole dataset withn3.Writer, and conditional-PUT with if-match and an explicitcontent-type: text/turtle. Handle 412 by re-fetching and re-applying.
Notes / caveats
SetFrom.subjectPredicate(this, iri, readMapper, writeMapper)returns a
live Set — adding/removing mutates the dataset (used bymarkRegistration).instancesOf(classIri, Ctor)is aprotectedhelper onDatasetWrapper— callable only from a subclass, which is whyTypeIndexDatasetexists rather than scanning quads by hand.- Use one
DataFactory(n3's) throughout, as elsewhere in the stack. - Public index = types discoverable by others (access may still be restricted);
private index = owner-only, behind WAC
(forum). - A registration may carry an
solid:instanceand be one of several entries
for a class; treatlocate()as returning a list. - SolidOS interop: the de-facto on-the-wire term is what the SHIPPING pane
reads/writes — verify against the pane source, not the spec/ns-doc. Type-Index
discovery only tells you where another app's data is; to actually read/write it
you must match the predicates that app uses, and a shipping SolidOS pane can
differ from its own ns-doc. Concretely: the SolidOS schedule pane
(`solid-panes/src/schedule/schedulePane.ts`)
reads and writessched:availabilty(misspelled, no secondi), whereas
the W3C ns-doc (http://www.w3.org/ns/pim/schedule#) definessched:availibility
(a different misspelling). To interoperate you must write the term the live
pane reads (sched:availabilty) and read tolerantly (accept both spellings).
General rule: for SolidOS interop, the de-facto term shipping in the pane wins over
the spec —grepthe pane source for the predicate before you hard-code it.
(Verified 2026-06 against the livesolid-panesmain; learned building the Pod
Manager/scheduleinterop, jeswr/solid-pod-managersrc/lib/schedule.ts.)
Type-Index-driven client-side pod search (no server FTS)
(An app that searches the user's whole pod across categories — a global
search box — without a server full-text index. Browser/client.)
A Solid pod has no built-in full-text search, so a cross-category "search my
pod" is a client-side scan seeded by the Type Index: it tells you which
containers hold what, so you scan those instead of crawling blindly. Four
rules, learned building the Pod Manager global pod search
(jeswr/solid-pod-manager #97):
Enumerate, scan, de-dupe. Build the source set from the user's
Type-Index-registeredsolid:instance/solid:instanceContainerlocations
(the read path above) plus your own app's first-party typed stores, then
scan each client-side and match in memory. De-dupe results by resource URL
— the same resource can be reached via a type registration and a
first-party path; let the typed (registered) entry win so a result is shown
once with its richer metadata.OWN-POD ONLY — SSRF-validate every discovered URL against the user's own
storages before fetching. The profile-linkedsolid:publicTypeIndex/solid:privateTypeIndex/pim:preferencesFileURLs (and thesolid:instance*locations inside an index) are values read from a
document — an attacker who can write a triple in the profile or index can
point them at a foreign origin. Before attaching the user's DPoP token,
confirm each URL falls under one of the user's ownpim:storageroots
(theisUnderStoragesegment-boundary check insolid-fetch-rdf); drop —
never fetch — any off-storage URL (attaching your DPoP token to a foreign
origin leaks it). Re-validate the registeredinstance/instanceContainer
locations too, not just the index URL — both come off attacker-writable
documents. (This is the own-pod-discovery mirror ofsolid-fetch-rdf's
cross-pod-claim / external-index provenance rules; same primitive.)Bound the scan so a large pod cannot hang the UI. Cap on three axes
together — max sources scanned, max results collected, and a wall-
clock time budget — and check the budget before each source and
before the type-index network discovery step (discovery is itself N
fetches). Stop early when any cap trips and surface "showing first N" /
"results may be incomplete" so a capped scan never looks like an empty pod.Render no raw RDF value as a link. Result titles/URLs are pod data; a
javascript:/data:IRI in an<a href>/<img src>is an injection vector
— scheme-filter tohttp(s):before rendering (same rule assolid-fetch-rdf
§"Consuming an external Hydra / TPF search index safely").
Keep the URL-validation and scheme checks in pure predicates (no fetch) so
the search filter is exhaustively unit-testable without a live pod — the same
testability discipline as the cross-pod provenance predicate in solid-fetch-rdf.
Cross-references
AGENTS.md§Writing data — read-modify-write, ETags, explicit Content-Type,
trailing-slash rule (containers end in/). This skill closes that
section's type-index deferral.- Package-local `solid-fetch-rdf` skill —
isUnderStorageown-storage containment check, the
cross-pod-claim provenance tiers, and the external-index SSRF/scheme rules the
pod-search section above reuses. solid-rdfskill — Turtle/JSON-LD parse + serialise, the "never hand-build
triples" rule.solid-server-matrix(proposed) — type-index seeding differs by server
(NSS auto-seeds; CSS does not), which is the whole reason fallback-and-create
is mandatory.
Could not verify
- Embedded-entity declaration in the registry (forum thread
t/how-should-embedded-entities-be-declared-in-the-type-index-registry/3023)
— out of scope here; not fetched. Exact— resolved: the worked example above@rdfjs/wrappermember names
was compiled (tsc --strict) against the published@rdfjs/wrapper0.34.0.d.ts(incl.instancesOf(klass: string, ctor)onDatasetWrapper,NamedNodeAs.string/NamedNodeFrom.string, 4-argSetFrom.subjectPredicate) on 2026-06-05.