List 'inspections' rows
const url = 'http://localhost:8080/api/inspections?select=label%3Amake%2Cmodel&order=id.desc&limit=50';const options = {method: 'GET', headers: {Prefer: 'count=exact'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'http://localhost:8080/api/inspections?select=label%3Amake%2Cmodel&order=id.desc&limit=50' \ --header 'Prefer: count=exact'Reads a page of rows the caller’s policy admits.
Filtering follows PostgREST’s spelling: every query parameter that is not one of the reserved names (order, limit, offset, after, select, or, and, not) names a field, and its value is <operator>.<operand> — ?year=gte.2020. The operators are eq, neq, gt, gte, lt, lte, like, ilike, in, is. in takes a bracketed candidate list (in.(skoda,vw)) and is takes exactly null, true or false; like/ilike take a pattern. An unrecognised operator is refused with 422, never quietly read as equality, and an ordering operator applied to a type this API defines no total order over is refused too — so one filter means the same thing on every engine.
Several parameters are one conjunction (AND), which is the only reading that keeps a filter narrowing as terms are added. or=(…) and and=(…) group terms explicitly and may nest, and prefixing any parameter name or group keyword with not. negates it (not.color=eq.red, not.or=(…)).
A field that is unavailable to the caller is refused exactly like one that does not exist, in filter, order and select alike — the refusal names the parameter’s role and never the field, so filtering, ordering or selecting cannot be used to discover a hidden field’s name. The one exception is a field the descriptor also marks required: its name is published in the write schemas below, because a mandatory field a caller cannot see could not be supplied — see the overview.
The response is an envelope — { "items": [ … ], "next": <cursor or null>, "count": <total or null> } — and never a bare array. All three members are always present: next is the cursor for the page after this one and is null on the last, and count is null unless the request opted into it. next is the only place that cursor appears: there is deliberately no Link or Content-Range header, so an agent reading the body never has to parse HTTP headers to keep paging.
A caller whose rule excludes every row is answered 200 with an empty page, not 403. A rule compiles to a row-level USING predicate, so a caller who fails it receives an allow carrying a predicate that matches nothing. A 403 here means something else entirely: the operation is unconfigured for this entity, the entity is unknown to the applied descriptor, the caller has no tenant on a tenant-scoped entity, or the policy reads a caller value this caller does not carry.
Neither precondition header is honoured on a list. A page has no version of its own to compare, so If-Match and If-None-Match are ignored here — not refused, as they would be on a write. Condition a single row’s read or write instead.
A nullable field is a sort key like any other, and nullslast is what it gets if you do not say otherwise. Where a null sorts is never left to the database: SQLite and PostgreSQL disagree on the default for a given direction, so the placement is always explicit in the statement Alvo emits and nullsfirst/nullslast are how you change it. Paging honours the same placement, so a cursor walks the null-keyed rows too — which was not true before: such a read used to be refused with 422 rather than answered, because a keyset boundary that compared the value alone dropped rows silently.
Sorting by a nullable field costs more than sorting by a required one. The null placement is emitted as a CASE expression over the key, which an index on that key cannot serve. Page by a required column where latency matters.
A Prefer: count preference is the only thing that fills the envelope’s count. It is the number of rows the query matches in total — narrowed by your policy and your filter, and not by limit, offset or after — so it does not shrink as you page. It is opt-in because it costs a second scan of the matching set on every request, and count is null on a request that did not ask. count=planned and count=estimated are accepted and degrade to an exact count: a planner estimate exists on one supported engine and not the other, and this API answers identically on both. What was applied comes back in Preference-Applied, and per RFC 7240 a preference this server does not recognise is ignored rather than refused — its absence from Preference-Applied is how that is reported.
The count is taken in a second statement over the same filtered set, not in the page’s own, because the page’s statement carries the cursor boundary and a count composed into it would report the rows after the cursor. So exact means “not an estimate”, not “atomically consistent with items”: a write landing between the two can make the number differ by one.
Authorizations
Section titled “Authorizations”- None
- alvoApiKey
Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”count=exact fills the page envelope’s count with the number of rows the query matches in total. Opt-in, because it is a second scan of the matching set on every request; a request sending no recognised count preference gets null there.
count=planned and count=estimated are accepted and degrade to an exact count, so they fill count too — a planner estimate exists on one supported engine and not the other, and this API answers identically on both. The response says which was applied in Preference-Applied, and it is always count=exact. Per RFC 7240 a preference this server does not recognise is ignored rather than refused, and its absence from Preference-Applied is how that is reported.
Example
count=exactQuery Parameters
Section titled “Query Parameters”Comma-separated field names to return, in the order named, each optionally renamed as alias:field. It narrows the read as well as the response: a field the projection does not name is not read from the row. Two groups of columns are read regardless — the framework-managed ones, and any field named in order, because no engine can sort by a column it did not read — but neither appears in the response unless the projection named it. A field the caller may not read is refused exactly as an undeclared one is. An alias is lower snake_case, is not the name of a framework-managed column, and cannot be claimed twice; and a projection cannot name more distinct keys than there are fields this caller can read.
Example
label:make,model<field>[.asc|.desc][.nullsfirst|.nullslast], comma-separated for several keys, outermost first. The modifiers must appear in that order and each at most once, so one sort key has one spelling; an unrecognised modifier is refused rather than ignored. A nullable field is a sort key like any other and defaults to nullslast; paging honours the same placement — see the operation description for what it costs.
Example
id.descHow many rows this page carries. A value past the maximum is refused, not clamped: a client that asked for more and silently received fewer computes its paging from a number no response ever told it. Zero is refused too — it is a read that can never return a row.
How many rows to skip. Prefer after: an offset re-scans the skipped rows and shifts under concurrent writes, where a keyset cursor does neither.
The keyset cursor a previous page returned as next, sent back verbatim. It is opaque and only the provider that issued it may interpret it, so it must not be decoded or constructed. A forged one yields an empty page rather than an error.
A bracketed, comma-separated list of terms combined as a disjunction (OR): or=(color.eq.red,make.in.(skoda,vw)). Groups may nest, and either the keyword or any member may carry the not. prefix. Repeating the parameter conjoins the groups.
A bracketed, comma-separated list of terms combined as a conjunction (AND): and=(color.eq.red,make.in.(skoda,vw)). Groups may nest, and either the keyword or any member may carry the not. prefix. Repeating the parameter conjoins the groups.
Filter on id, as <operator>.<operand>. See the operation description for the operators, the not. prefix and how several parameters combine.
Filter on vehicle_id, as <operator>.<operand>. See the operation description for the operators, the not. prefix and how several parameters combine.
Filter on inspector_name, as <operator>.<operand>. See the operation description for the operators, the not. prefix and how several parameters combine.
Filter on inspected_on, as <operator>.<operand>. See the operation description for the operators, the not. prefix and how several parameters combine.
Filter on passed, as <operator>.<operand>. See the operation description for the operators, the not. prefix and how several parameters combine.
Filter on notes, as <operator>.<operand>. See the operation description for the operators, the not. prefix and how several parameters combine.
Responses
Section titled “Responses”A page of rows the caller’s policy admits.
One page of rows, the cursor that reads the page after it, and — when the request opted in with a Prefer: count preference — how many rows the query matches in total.
object
The rows in this page, in the requested order.
A roadworthiness inspection performed on a vehicle.
One row inside a list’s page. Every field is present unless the request narrowed the projection with select; a field with no value is present and null. Fields marked read-only are written by the framework and refused in a request body.
object
The opaque cursor for the next page, or null when this page is the last. Send it back verbatim as after; it is the provider’s to interpret and must not be decoded.
How many rows the query matches in total — not the size of this page — or null unless the request sent a recognised Prefer: count preference — exact, or planned/estimated, which degrade to an exact count. It is narrowed by the caller’s policy and by the filter, and not by limit, offset or after, so it does not shrink as you page. Opt-in because an exact count is a second scan of the matching set on every request. Exact means “not an estimate”, not “consistent with items”: it is taken in a second statement, so a write landing between the two can make it differ from the rows by one.
Example
{ "items": [ { "id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "vehicle_id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "inspected_on": "2026-01-31" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeWhat was done with the request’s Prefer header (RFC 7240 §3). Present only when a preference was applied, and always count=exact — count=planned and count=estimated degrade to a real count, and this is where a caller who asked for an estimate learns they received the exact one. Absent means no preference was applied, which is how RFC 7240 reports one this server does not recognise.
Example
count=exactA credential was presented and cannot be used — unknown, revoked, expired, malformed, or issued for another tenant. One wording for all of them, so key ids cannot be enumerated one request at a time. ‘WWW-Authenticate’ names the scheme and the header to send. Presenting no credential at all is not a 401: an anonymous caller is judged by policy like any other.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeThe RFC 7235 challenge, naming the scheme and the request header a credential is read from — so an agent can discover how to authenticate rather than guess. The header name is host configuration, which is why the challenge states it rather than assuming a default.
The operation is refused. Two kinds, told apart by the problem ‘type’ and never by its prose: ‘out-of-scope’ means the presented key’s scopes do not cover this entity and operation (grant the key the scope), and ‘forbidden’ means policy refused it (change a rule). A policy refusal here is an operation-level one — the operation is unconfigured, the entity is unknown to the applied descriptor, the caller has no tenant on a tenant-scoped entity, or the policy reads a caller value this caller does not carry. It is never ‘your rule excluded these rows’; see the 200.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeThe request could not be acted on: a query string or body that is malformed, a body the entity’s declared shape refuses, or a header this API cannot honour. The ‘violations’ array carries every reason at once — a pointer, a stable code and a fix suggestion each — never only the first, so one round trip is enough to repair the request.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-store