Querying
Filter with where, sort with order_by, paginate and count.
The list endpoint, GET /v1/{realm}/{database}/{collection}, accepts these query parameters:
| Parameter | Default | Meaning |
|---|---|---|
where | — | JSON object with filter conditions |
order_by | _meta.created_at,id | Comma-separated fields; prefix a field with - for descending |
limit | 20 | Page size, 1–100 |
skip | 0 | Number of documents to skip |
after | none | The next_cursor of the previous page: continues the list after it (see Paging with a cursor). Can’t be combined with skip |
count | false | true adds total, the number of documents matching where |
deleted | none | In a collection with soft delete: only lists the deleted documents (the trash), include both. Needs the restore rule besides read |
Each parameter may appear at most once. Any other parameter returns 400 invalid_query.
GET /v1/shop/orders/items?where={"status":"active","price":{"$between":[10,20]}}&order_by=-_meta.created_at&limit=10&count=true
{ "items": [ ... ], "limit": 10, "skip": 0, "has_more": true, "next_cursor": "…", "total": 134 }
The example is shown unencoded for readability.
where
- Top-level keys are field paths. Use dot notation for nested fields, for example
address.city. - Multiple keys, and multiple operators on one field, are combined with AND. Use
$orfor alternatives. - A bare value is shorthand for
$eq:{"status": "active"}is the same as{"status": {"$eq": "active"}}.
{
"status": "active",
"price": { "$gte": 10, "$lt": 50 },
"name": { "$istartsWith": "wid" },
"discount": { "$isNull": true }
}
Operators
| Group | Operator | Value | Matches when the field… |
|---|---|---|---|
| Comparison | $eq, $ne | value | equals / doesn’t equal the value |
$gt, $gte, $lt, $lte | number or string | is greater / less than the value | |
| Sets & ranges | $in, $nin | array | is / isn’t one of the values |
$between | [min, max] | is within the range, inclusive | |
| Null | $isNull | true / false | is null or missing / has a non-null value |
$notNull | true / false | the opposite of $isNull | |
| Text | $like, $ilike | pattern | matches the whole pattern: % is any run of characters, _ is exactly one character |
$startsWith, $istartsWith | string | starts with the value | |
$endsWith, $iendsWith | string | ends with the value | |
$contains, $icontains | string | contains the value |
- Operators prefixed with
iare case-insensitive. All other text matching is case-sensitive. - Text operators apply only to string fields.
- All characters in text values are literal, except
%and_in$like/$ilike. There is no escape character for them. $ne,$ninand$eq: nullalso match documents where the field is missing, as does$isNull: true.
Array fields
- A scalar value matches if any element matches. For example,
{"tags": "sale"}finds documents whosetagscontain"sale". - An array value matches the whole array exactly, including order.
- Properties of objects inside arrays can be queried by dot path, for example
lines.sku.
$or and $and
$or matches documents that match any of the objects in its array; $and matches documents that match all of them. Each object in the array is a where object of its own, with the same fields, operators and checks:
{
"published": true,
"$or": [
{ "title": { "$icontains": "go" } },
{ "body": { "$icontains": "go" } }
]
}
This finds published documents whose title or body contains “go”. Keys next to $or still combine with AND.
- Groups can be nested, for example an
$andinside an$or, up to 4 levels deep. - The array must not be empty, and neither may its objects.
$andis rarely needed, because keys already combine with AND. It’s useful inside an$orbranch, or to put two alternatives side by side:{"$and": [{"$or": [...]}, {"$or": [...]}]}.- The limits apply to the whole
where: conditions in every group count towards the 32. - In realms with access rules,
$oronly narrows what the caller may read. It can never widen it, because the read rule is always applied on top. - Other keys starting with
$(such as$nor,$notor$where) are rejected.
Errors inside groups name their position, for example where.$or.1.body.$icontains.
Fields and types
Every field in where and order_by must be one of:
- declared in the collection’s
schema.json; id;_meta.created_at,_meta.updated_ator_meta.version;_meta.owner,_meta.created_byor_meta.updated_by(strings;_meta.ownercan benull). Only documents in realms withauth: enabledhave them; elsewhere these fields are always missing.
Values are checked against the field’s schema type:
- a string for an
integerfield is rejected; 1.5for anintegerfield is rejected;nullis only accepted for fields whose type includesnull(use$isNullotherwise);_meta.created_atand_meta.updated_attake RFC3339 timestamps and are compared as dates, for example{"_meta.created_at": {"$gte": "2026-09-01T00:00:00Z"}}. So do the fields the schema stores as dates (x-backd-store), in any offset; text operators are refused on them.
Limits
| Limit | Value |
|---|---|
Size of where | 4096 bytes |
Conditions (operators) in where | 32 |
| Length of a text operator value | 256 characters |
Wildcards (%, _) in a $like / $ilike pattern | 4 |
Fields in order_by | 8 |
Nesting of $or / $and | 4 levels |
Text of a $search | 256 bytes, 10 terms |
Full-text search
A collection with a text index can be searched with $search, which, unlike $contains, uses the index:
GET /v1/shop/blog/articles?where={"$search":"red \"running shoes\" -kids","status":"published"}&order_by=_relevance&limit=20
- Placement.
$searchis a key at the top level ofwhere, ANDed with the other conditions and with the read rules. Inside$oror$andit is400 invalid_query: the database can’t combine a text search with alternatives. A collection with no text index answers400 search_not_enabled. - Syntax. Words (a document matches any of them),
"quoted phrases"and-excludedwords. It is a plain string: nothing in it is read as an operator or a pattern. At most 256 bytes and 10 terms (a phrase is one). - Language. Words are stemmed in the language of the request:
language=(a locale code such asesoren-GB), else the signed-in user’s locale, else the first language ofAccept-Languagefor anonymous callers, else the index’sdefault_language. A locale with no language of its own searches without stemming. Documents are stemmed in their own language, from the index’slanguage_field. - Relevance.
order_by=_relevance, alone, orders by how well documents match (title weights beat body weights), ties byid, and puts the score in each document’s_meta.score. Relevance pages byskip, and only through the first 1,000 results (400beyond); there is noafter. Any otherorder_byworks as usual, with the search as a filter. - Elsewhere.
$searchalso works in aggregations (results per category next to a search box) and withexpand. Conditional GET works as for any list. - Cost. A search that needs more than
BACKD_AGGREGATE_TIME(5 seconds by default) is422 search_too_expensive: many common words match most of a large collection and have to be scored. Fewer or rarer words, or a narrowerwhere, fix it. - While the index is rebuilt (its declaration changed and
backd provisionis replacing it) a search answers503 index_buildingwithRetry-After.
The search guide covers declaring the index, weights, languages and what search doesn’t do.
$search or $contains? $contains and $icontains look for an exact substring and read every document of the collection: good for a few thousand documents and for partial words. $search is for words and phrases in prose, with stemming and ranking, and stays fast on large collections. It doesn’t find partial words (runn), tolerate typos or return facets.
Errors
Invalid queries return 400 invalid_query, with one detail per problem:
{
"error": {
"code": "invalid_query",
"message": "invalid query parameters",
"details": [
{ "path": "where.age.$gt", "reason": "must be of type integer" },
{ "path": "where.secret", "reason": "unknown field" },
{ "path": "order_by.nope", "reason": "unknown field" }
],
"request_id": "d2fk1a30h2ipb32i2hh0"
}
}
Ordering and pagination
order_by=-age,namesorts byagedescending, thennameascending.idascending is always added as a final tiebreaker unlessorder_byalready includesid, so pages are stable.- Without
order_by, documents come in creation order:_meta.created_at, thenid. Ids that clients choose (custom ids) have no order of their own, so the default doesn’t lean on them.backd provisioncreates an index for it on every collection. Earlier versions ordered byid, which for generated ids is nearly the same. has_moreistruewhen documents exist beyond this page.skippages by offset, so deep pages get slower and can shift if data changes between requests. A cursor doesn’t have those problems.
Paging with a cursor
When has_more is true, the answer has a next_cursor. Send it as after, with the same where and order_by, to get the next page:
GET /v1/shop/orders/items?order_by=-price,name&limit=50
GET /v1/shop/orders/items?order_by=-price,name&limit=50&after=eyJvIjoi…
- A cursor is a position, not a reading of the document it came from: it keeps working if that document is edited or deleted meanwhile, so a loop that processes and deletes documents is safe, and documents added or removed elsewhere never shift a page. Each page costs the same however far you are, if the order is indexed.
- Treat it as opaque. It is an encoded position that only continues the same
order_by. The one exception isorder_by=idon its own, where it is just theidof the last document, soafter=<id>continues after any document you know. A cursor of anotherorder_by, anotherorder_by, a made-up value orskipanswers400 invalid_queryonafter. - Equal values and missing or
nullvalues are handled: documents with the same sort value are told apart byid, andnullor missing values sort first, as in a normal list. - It follows
order_byfields that hold one kind of scalar value (strings, numbers, booleans, dates, ids). For fields that hold arrays, objects or several types there is nonext_cursor, andafteris refused: page those withskip. - It never shows more than the first page would:
whereand the read rules apply to everything after the cursor.count=truestill counts the whole list, not what is left. - The
audit,jobsandinvocationslists of the admin API still page withskip.