This document extends the Linked Web Storage (LWS) protocol with server-managed services for type-based discovery: a Type Index Service that enumerates the distinct resource types present within a storage, and a Type Search Service that returns the resources matching a type filter. The specification defines how servers derive types, the query model and its error handling, and the authorization requirements that keep responses client-specific. Both services are discoverable via the storage description resource.
This is an unofficial proposal.
The [[!LWS10-CORE|Linked Web Storage]] protocol defines a resource-centric storage system built on HTTP. While the core protocol supports creating, reading, updating, and deleting resources, and containers enumerate the resources they contain, it does not define a mechanism for a client to learn which kinds of resources exist within a storage, or to locate the resources of a given kind, other than by traversing the containment hierarchy one resource at a time.
This specification extends LWS with two server-managed services, discoverable via
the storage description resource. The TypeIndexService provides a
server-managed discovery mechanism to query the distinct resource types available
within a storage, while the TypeSearchService allows clients to retrieve
the specific resource URIs matching those types.
The terms "storage", "storage description resource", "container", "data resource", "resource manager", and "requesting agent" are defined by [[!LWS10-CORE]].
This specification defines the following additional terms:
TypeIndexService service type, that enumerates the distinct resource
types present within a storage as visible to the requesting client.
TypeSearchService service type, that returns descriptions of the
resources within a storage matching a filter over types and
indexed relations, as visible to the requesting client.
type.
A storage MAY support the Type Index Service, the
Type Search Service, or both. A storage that supports one of these services
MUST advertise it with a service object in the service array of its
storage description resource: the entry for the Type Index Service MUST have
a type equal to TypeIndexService, and the entry for the
Type Search Service MUST have a type equal to
TypeSearchService.
Each service object MUST include a serviceEndpoint property whose value
is the URI of the corresponding service endpoint. Additional properties MAY be
present on a service object.
{
"@context": "https://www.w3.org/ns/lws/v1",
"id": "https://example.org/storage/alice/",
"type": "Storage",
"service": [
{
"type": "TypeIndexService",
"serviceEndpoint": "https://example.org/types/index"
},
{
"type": "TypeSearchService",
"serviceEndpoint": "https://example.org/types/search"
}
]
}
The storage description advertises only the service endpoints. In particular, the set of additional indexed relations a server supports is deliberately not enumerated there or anywhere else; see .
To prevent inadvertent or malicious corruption by clients, these services are
strictly populated and managed by the server. To mitigate security and performance
risks associated with deep-parsing arbitrary resource bodies, servers are not
required to parse resource content to discover types. Instead, servers SHOULD derive
resource types from HTTP Link headers provided by the client during resource creation
or modification, combined with the server's intrinsic knowledge of the resource's
state (such as native LWS classes like
https://www.w3.org/ns/lws#Container).
Servers MAY additionally derive types from the resource representation itself when they are able to parse it. Implementations are encouraged to do so where feasible, as richer type discovery improves the utility of the Type Index and Type Search services. When a server enriches the type index from resource content, the types it surfaces MUST be treated identically to those derived from Link headers for the purposes of indexing, search, and authorization filtering.
Type and indexed-relation membership in these services MAY be eventually consistent. Following a write that changes a resource's derivable types or relation targets (creation, update, or deletion), a server SHOULD reflect the change within a bounded, implementation-defined interval; until it does, a response MAY omit a newly matching resource or briefly retain one that no longer matches. Clients MUST NOT assume read-your-writes consistency and SHOULD tolerate transient staleness. This allowance applies only to derivation and MUST NOT be applied to authorization filtering (see ).
Because servers are not required to parse the resource body, the Type Search Service
relies heavily on the types explicitly declared via HTTP headers or managed by the
server. If a client includes a type exclusively within a resource's internal graph
but fails to provide the corresponding Link header, that type may not
be discoverable via these services.
The Type Index Service provides a server-managed discovery mechanism to query the distinct resource types available within a storage.
Returns a paginated TypeIndex as application/lws+json,
following the standard LWS pagination model defined by [[!LWS10-CORE]]: page URIs
are conveyed in Link headers—not in the response body—and
are opaque to the client. The body lists the unique types of resources that
currently exist within the storage and that the authenticated user is authorized to
see. This specification defines no query parameters for the endpoint: a client
requests the advertised serviceEndpoint URI as-is, and any parameters
appearing in server-issued pagination URIs (such as ?page= in the
example below) are server-internal parts of those opaque URIs, not part of this
interface. A page reference may expire or cease to be recognized; the
pagination-expiry rule of applies to
TypeIndex pages as it does to search result pages.
The Type Index takes no filter, so it is retrieved with a GET. The
Type Search Service, which carries a filter, instead uses the HTTP
QUERY method [[!RFC10008]] (see ).
Request
GET /types/index
Authorization: Bearer <token>
Response
HTTP/1.1 200 OK
Content-Type: application/lws+json
Cache-Control: private
Link: <https://example.org/types/index?page=1>; rel="first"
Link: <https://example.org/types/index?page=2>; rel="next"
Link: <https://example.org/types/index?page=4>; rel="last"
{
"@context": "https://www.w3.org/ns/lws/v1",
"type": "TypeIndex",
"totalItems": 142,
"items": [
{ "id": "https://schema.org/Person" },
{ "id": "https://schema.org/Event" },
{ "id": "https://schema.org/Message" }
]
}
The Type Search Service allows clients to retrieve descriptions of the resources within a storage matching a filter over types and indexed relations.
A client searches by sending an HTTP QUERY request [[!RFC10008]] to the
service endpoint, carrying the filter in the request body. The query format is identified
by the request's Content-Type header field, which MUST be present; per
[[!RFC10008]], a server fails the request if the Content-Type is missing or
inconsistent with the body (see ). Every server MUST
support the application/lws-query+json filter format defined here as its baseline query
format; a server MAY additionally accept other query formats, each identified by its own
media type, and the authorization requirements of
apply regardless of the format used. The QUERY method is both safe and
idempotent [[!RFC10008]], in the sense of those terms as defined by [[!RFC9110]]: a
search never alters server state, and it MAY be repeated, retried, and/or cached.
Because the filter travels in the body rather than the request URI, it is not subject to the
request-URI length limits that constrain a query string.
The baseline filter format is identified by the media type
application/lws-query+json, registered in
. A filter document is a single JSON object
[[!RFC8259]] whose members express constraints: the type member and,
optionally, one member per filtered link relation, each carrying the
conjunctive-normal-form value grammar defined below. Unlike the
application/lws+json representations defined by [[!LWS10-CORE]] —
which this service still uses for its responses — a filter document is
plain JSON rather than JSON-LD: it carries no @context, its member names
are not JSON-LD terms, and its nested-array groups are not subject to JSON-LD
constraints on @type values. A member whose name begins with
@ (a JSON-LD keyword such as @context) MUST be ignored; it
is neither a constraint nor a relation key.
A server SHOULD advertise the query formats it accepts using the
Accept-Query response header field [[!RFC10008]] — for example in
response to an OPTIONS request on the service endpoint, whose Allow
header also signals that the QUERY method is supported. Accept-Query
enumerates only the query-format media types the server understands; it MUST NOT be used to
disclose which indexed relations or filter capabilities a server supports, which
remain deliberately unobservable (see ). A client that
receives a 415 (Unsupported Media Type) response MAY consult its
Accept-Query value to select a supported format.
{
"type": [
["https://schema.org/Person", "http://xmlns.com/foaf/0.1/Person"],
"https://www.w3.org/ns/lws#DataResource"
]
}
The type key is OPTIONAL and selects resources by their rdf:type
(as declared via rel="type" Link headers, server-intrinsic
classes, or content; see ). Its value is an array whose
elements are combined with logical AND; each element is either a single
type IRI [[!RFC3987]] or an array of type IRIs combined with logical OR. The example
above selects resources matching
(schema:Person OR foaf:Person) AND lws:DataResource — the filter is a
conjunction of disjunctions; that is, conjunctive normal form. A request whose body has no
type key and no relation keys (or all of whose keys have values of empty arrays)
matches all resources visible to the client.
A single-type query — a type value containing one IRI and no OR group,
for example {"type": ["https://schema.org/Person"]} — matches every
resource that declares that type, even if the resource also declares other types.
A type filter always denotes the type the matched resource itself bears; it never denotes the types of a container's members. The native LWS classes https://www.w3.org/ns/lws#DataResource and https://www.w3.org/ns/lws#Container are ordinary type values: filtering on them selects resources that are themselves data resources or containers, respectively (for example, a type of https://www.w3.org/ns/lws#Container matches container resources, and a filter of https://schema.org/Person AND https://www.w3.org/ns/lws#DataResource matches data resources that are also schema:Person).
This conjunctive-normal-form filter is the complete query expressiveness the service is
required to provide. It is also the complete expressiveness of the
application/lws-query+json format itself: a server MUST NOT extend this
grammar. Richer expressiveness — such as nesting, negation, comparison, ordering, or
text matching — is offered only through an alternate query format identified by its
own media type and advertised in Accept-Query, and a portable client MUST NOT
rely on any query format beyond this baseline. A type IRI that matches no
resource yields no results and MUST NOT be treated as an error, and duplicate groups MUST
be ignored. An empty group — an empty array appearing as an element of a filter
key's value — MUST be rejected with 400 (Bad Request): an empty
disjunction can match nothing, so ignoring it would silently broaden the query beyond its
logical meaning (see ). A filter key whose entire value
is an empty array expresses no constraint and is treated as if the key were absent.
The type key is the mandatory baseline and MUST be supported by every server
that implements the TypeSearchService. A server MAY additionally index other
descriptive link relations. Such a relation is filtered by using the relation
itself as a key alongside type — a registered relation name per
[[!RFC8288]] or, for an extension relation, its URI — with exactly the same
conjunctive-normal-form semantics as type (a nested array is an OR group,
sibling array elements form AND groups, target values matched against the link targets the
resource declares for that relation). Groups belonging to different relations are combined
with logical AND. Relation targets are derived by the server in the same way as types (HTTP
Link headers, server-intrinsic state, or parsed content) and all sources MUST
be treated identically.
A server MUST NOT enumerate its set of additional indexed relations — not in the storage description nor elsewhere. A relation the server does not index, like a target value that matches nothing, yields no results for that constraint: this MUST NOT be treated as an error, and the two cases MUST be indistinguishable to the client (consistent with the type-matching rule above). Only descriptive relations are eligible for indexing; servers MUST NOT index structural or protocol relations.
Withholding the set of indexed relations keeps the filter interface from serving as a discovery oracle for the server's configuration; see .
A successful search returns 200 (OK). The default response is an
application/lws+json ContainerPage whose items describe the
matching LWS resources, and every server MUST be able to produce it: a request that sends
no Accept header field, or one that admits application/lws+json,
receives this representation. A server MAY honor an Accept header field that
requests a different response media type and return the result set in that format instead
[[!RFC10008]]; if it can satisfy none of the media types the request finds acceptable, it
responds 406 (Not Acceptable) (see ).
Each item in a ContainerPage result MUST carry at least the matched
resource's id and its type, mirroring a container member; a
server MAY include additional descriptive metadata (such as mediaType,
size, or modified) but is not required to.
Whatever the response format, the result set is paginated according to the standard LWS
pagination model defined by [[!LWS10-CORE]]: page URIs are conveyed in Link
headers (not in the response body), are opaque to the client, and are
dereferenced to retrieve subsequent pages. Aside from type and any additional
relation keys described above, no other filter inputs are defined.
A ContainerPage returned by the TypeSearchService is a synthetic result set, not a representation of a container resource. The ContainerPage media type and pagination model are reused only for client convenience; the response does not identify a container, has no containment relationship to its members, is not retrievable or mutable as a resource, and its membership reflects the query rather than any stored hierarchy.
[[!RFC10008]] permits a response to carry a Content-Location header field
identifying a resource that represents the query results. Because a Type Search result set
is synthetic and client-specific (see the note above and
), this specification does not define such a resource;
a server that nonetheless exposes one MUST subject it to the same authorization filtering on
every access and MUST prevent its reuse across clients.
Servers indicate request outcomes using standard HTTP status codes [[!RFC9110]]. In particular:
application/lws-query+json, or whose
type is not an array, or any of whose elements is neither a string nor a non-empty array of strings, MUST be rejected with 400 (Bad Request). In particular, an empty group is rejected, not ignored (see ).
Content-Type identifies a query format the server does not support MUST be rejected with 415 (Unsupported Media Type), and such a response SHOULD include an Accept-Query header field [[!RFC10008]] listing the query formats the server does accept; every server supports at least application/lws-query+json (see ). A request that omits the Content-Type header field, which [[!RFC10008]] requires a server to reject, MUST be rejected with 400 (Bad Request).
Accept header field excludes every response media type the server can produce for the result set MUST be rejected with 406 (Not Acceptable) [[!RFC10008]]. Because every server can produce the application/lws+json ContainerPage, a request that omits Accept or admits that media type never results in 406.
type value, or any relation target value, that does not match the
IRI production of [[!RFC3987]] — an absolute IRI, optionally carrying
a fragment identifier, the form of every IRI in the RDF abstract syntax
[[rdf11-concepts]] — MUST be rejected with 400 (Bad Request). This is distinct from a
well-formed IRI that matches no resource, and from a relation the server does not index, both of which yield no results and are not errors (see above).
422 (Unprocessable Content), the status [[!RFC9110]] defines and [[!RFC10008]] suggests for a query that is understood but cannot be processed due to its actual contents. A server MUST NOT silently truncate or otherwise narrow such a filter, as doing so could return a superset of the intended results.
404 (Not Found) or
410 (Gone). This rule applies to the pages of both services: those of a
Type Search Service result set and those of a Type Index Service listing
alike. Clients SHOULD restart — a search by re-sending the QUERY
request, a type index listing by re-requesting the service endpoint.
Error responses may include a representation describing the problem; this specification does not define its format.
A QUERY response is cacheable [[!RFC10008]]. Because the filter travels in the
request body, it forms part of the cache key itself ([[!RFC10008]], Section 2.7) and cannot
be conveyed through the Vary header field, which names only request header
fields. A server that negotiates the response representation on Accept (see
) MUST list Accept in Vary so that
caches do not reuse a representation across response formats. Because every result set is
authorization-filtered and therefore client-specific, the cross-client caching requirements
in also apply.
Request
GET /types/index
Authorization: Bearer <token>
Response
HTTP/1.1 200 OK
Content-Type: application/lws+json
Cache-Control: private
Link: <https://example.org/types/index?page=1>; rel="first"
Link: <https://example.org/types/index?page=2>; rel="next"
Link: <https://example.org/types/index?page=4>; rel="last"
{
"@context": "https://www.w3.org/ns/lws/v1",
"type": "TypeIndex",
"totalItems": 142,
"items": [
{ "id": "https://schema.org/Person" },
{ "id": "https://schema.org/Event" }
]
}
Request
OPTIONS /types/search
Response
HTTP/1.1 204 No Content
Allow: OPTIONS, QUERY
Accept-Query: application/lws-query+json
A server that also accepted other query formats would list them in Accept-Query,
for example: Accept-Query: application/lws-query+json, application/sparql-query
Request
QUERY /types/search
Authorization: Bearer <token>
Content-Type: application/lws-query+json
{
"type": [
"https://schema.org/Person",
"https://www.w3.org/ns/lws#DataResource"
]
}
Response
HTTP/1.1 200 OK
Content-Type: application/lws+json
Cache-Control: private
Link: <https://example.org/types/search?cursor=8f2a1c>; rel="first"
Link: <https://example.org/types/search?cursor=b71e90>; rel="next"
{
"@context": "https://www.w3.org/ns/lws/v1",
"type": "ContainerPage",
"totalItems": 27,
"items": [
{ "type": ["DataResource", "https://schema.org/Person"], "id": "https://example.org/data/person-4872" },
{ "type": ["DataResource", "https://schema.org/Person"], "id": "https://example.org/data/person-4873" },
{ "type": ["DataResource", "https://schema.org/Person"], "id": "https://example.org/data/person-5882" }
]
}
Request
QUERY /types/search
Authorization: Bearer <token>
Content-Type: application/lws-query+json
{
"type": ["https://www.w3.org/ns/lws#Container"]
}
Matches resources whose type includes lws:Container. This does NOT mean
"containers that contain something" - the service has no containment query.
The returned URIs are container resources because each is itself a lws:Container.
Response
{
"@context": "https://www.w3.org/ns/lws/v1",
"type": "ContainerPage",
"totalItems": 2,
"items": [
{ "type": "Container", "id": "https://example.org/data/people/" },
{ "type": "Container", "id": "https://example.org/data/contacts/" }
]
}
Request (nested array = OR group, outer array = AND)
QUERY /types/search
Authorization: Bearer <token>
Content-Type: application/lws-query+json
{
"type": [
["https://schema.org/Person", "http://xmlns.com/foaf/0.1/Person"],
"https://www.w3.org/ns/lws#DataResource"
]
}
Selects resources matching (schema:Person OR foaf:Person) AND lws:DataResource.
Response
{
"@context": "https://www.w3.org/ns/lws/v1",
"type": "ContainerPage",
"totalItems": 2,
"items": [
{ "type": ["DataResource", "https://schema.org/Person"], "id": "https://example.org/data/person-4872" },
{ "type": ["DataResource", "http://xmlns.com/foaf/0.1/Person"], "id": "https://example.org/data/contact-2208" }
]
}
Request (type AND a 'describedby' relation, the relation name used as a key)
QUERY /types/search
Authorization: Bearer <token>
Content-Type: application/lws-query+json
{
"type": ["https://schema.org/Person"],
"describedby": ["https://shapes.example/PersonShape"]
}
Selects resources that are schema:Person AND declare a 'describedby' link to the given shape.
If the server does not index 'describedby', the constraint yields no results (not an error,
and indistinguishable from no resource declaring that link).
Response
{
"@context": "https://www.w3.org/ns/lws/v1",
"type": "ContainerPage",
"totalItems": 2,
"items": [
{ "type": ["DataResource", "https://schema.org/Person"], "id": "https://example.org/data/person-4872" },
{ "type": ["DataResource", "https://schema.org/Person"], "id": "https://example.org/data/person-5012" }
]
}
422 as required by
rather than narrowing them.
TypeIndexService
GET response under the ordinary HTTP caching rules [[RFC9110]], and a
TypeSearchService QUERY response per [[RFC10008]], whose cache
key is computed over the request body rather than the requesting client. Serving either
through a shared cache risks delivering one client's authorization-filtered view to
another. The cache requirements in exist to prevent
this, and deserve particular attention when a shared cache sits in front of either
service.
This document defines the following terms in the
https://www.w3.org/ns/lws# namespace:
| Term | URI | Description |
|---|---|---|
TypeIndexService |
lws:TypeIndexService |
Service type for type index discovery |
TypeSearchService |
lws:TypeSearchService |
Service type for type search discovery |
TypeIndex |
lws:TypeIndex |
Document type returned by the Type Index Service |
This document also uses the terms Storage, ContainerPage,
service, serviceEndpoint, totalItems, and
items defined by [[!LWS10-CORE]] and the LWS JSON-LD context.
application/lws-query+json Media Type
This specification registers the application/lws-query+json media type,
identifying the baseline Type Search filter format defined in
.
application/lws-query+json
media type are required to conform to all of the requirements for the
application/json media type and are therefore subject to the same encoding
considerations specified in Section 11 of [[RFC8259]].@ are ignored (see
).QUERY messages, but is not restricted to that context.