Skip to content
Alvo is pre-v0.1: the image runs from its edge tag, and no NuGet package or release is published yet.Pre-v0.1: no release yet.Roadmap and status

Query 'owners' rows through a request body

POST
/api/owners/query
curl --request POST \
--url http://localhost:8080/api/owners/query \
--header 'Content-Type: application/json' \
--header 'Prefer: count=exact' \
--data '{ "select": "example", "order": "example", "limit": 50, "offset": 1, "after": "example", "or": [ "example" ], "and": [ "example" ], "id": [ "example" ], "name": [ "example" ], "email": [ "example" ], "phone": [ "example" ], "created_at": [ "example" ], "created_by": [ "example" ], "updated_at": [ "example" ], "updated_by": [ "example" ] }'

Reads a page of rows the caller’s policy admits, taking the same parameters in a JSON request body.

It exists for one reason: a filter a request line cannot carry. Alvo’s own budgets are generous — 256 filter terms and 1000 in candidates — and a proxy’s URL limit is reached first, so ?id=in.(…400 ids…) is refused by an intermediary with a 414 carrying no violations array at all. Sent as a body it is answered normally.

The body is a JSON object whose members are the query parameters, and the grammar inside each value is exactly the one the query string carries: {"year": "gte.2020", "or": ["(color.eq.red,color.eq.blue)"], "select": "id,label:make", "limit": 50}. A repeated parameter is an array of strings; the same name twice in one object is refused, because JSON leaves the order of two such members undefined. {} is the empty query — every readable field, the default page.

Values are not percent-encoded here, and that is the point. A query string carries the escaping of a value; a JSON string carries the value. So {"make": "like.100%"} is what ?make=like.100%25 means, and + is a plus rather than a space. Everything else is identical: the same parser, the same refusals, the same page envelope, the same Prefer: count preference.

This is a read and is gated as list. A caller whose list is unconfigured is refused here exactly as on the collection GET, before the body is read at all — so a refusal never arrives dressed as a complaint about the body.

Idempotency-Key is accepted and ignored. There is nothing to make idempotent: no row is written, so a retry costs a second read and nothing else. It is accepted rather than refused because several SDKs attach it to every POST.

A refusal’s pointer tells you where to look: an empty string or one beginning with / is a JSON Pointer into this body, and any other value is the role of a query parameter — filter, order, limit, offset, after or select.

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.

Prefer
string

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=exact

The query parameters, as an object. An empty object reads the first page with no filter.

Media typeapplication/json

The query parameters, as an object. A member’s name is a parameter and its value is the text a query string would carry; an array repeats the parameter. See the operation description for the grammar and for what a field property accepts.

object
select
string
order
string
limit
integer format: int32
default: 50 >= 1 <= 200
offset
integer format: int32
after
string
>= 1 characters <= 512 characters
or
One of:
string
and
One of:
string
id
One of:
string
name
One of:
string
email
One of:
string
phone
One of:
string
created_at
One of:
string
created_by
One of:
string
updated_at
One of:
string
updated_by
One of:
string

A page of rows the caller’s policy admits.

Media typeapplication/json
ownersPage

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
items
required

The rows in this page, in the requested order.

Array<object>
ownersPageItem

A person or company a vehicle is registered to.

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
id
string format: uuid
name
string
<= 120 characters
email
null | string format: email
/^(?:[^@\s]+@[^@\s.]+(\.[^@\s.]+)+)$/
phone
null | string format: phone
/^(?:\+?[0-9][0-9 ()./\-]{3,30})$/
created_at
string format: date-time
created_by
null | string format: uuid
updated_at
string format: date-time
updated_by
null | string format: uuid
next
required

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.

null | string
<= 512 characters
count
required

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.

null | integer format: int64

Example

{
"items": [
{
"id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04",
"email": "someone@example.com",
"phone": "+421 900 123 456",
"created_at": "2026-01-31T09:30:00+00:00",
"created_by": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04",
"updated_at": "2026-01-31T09:30:00+00:00",
"updated_by": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04"
}
]
}
Cache-Control
string

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
ETag
string

The row’s version as a strong entity tag. Send it back verbatim as If-Match to make a later write conditional; it is compared octet-for-octet, so it must not be reformatted. Absent when the row has no version this caller can be given.

Example
"638712345678900000"
Preference-Applied
string

What 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=exact

A 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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
WWW-Authenticate
string

The 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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

The request body was not declared as JSON, or carried no ‘Content-Type’ at all. Send ‘Content-Type: application/json’, or any ‘application/*+json’; a POST refusal also carries ‘Accept-Post’ and a PATCH refusal ‘Accept-Patch’ naming what the operation accepts. The requirement exists because a body-taking route with no media-type requirement is reachable as a CORS simple request, and a host with its own cross-site-request-forgery defence can turn it off.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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

The 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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