GET /app/{orgCode}/list/{entity} is the query layer, and it runs on DynamoDB — immediately consistent and available on every instance. Any field can be filtered with no setup; a lookup field, name, created_by, updated_by, the parent key and the owner scope use an index and stay fast on any amount of data. Only full-text search (?q=) and facets need the OpenSearch add-on (Enterprise).

Common query parameters

http
GET /list/order                          default list
GET /list/order?sortby=name&sortdir=desc     sort across all records (name, created_at, updated_at, id)
GET /list/order?status=active            filter by any field
GET /list/order?price=100&price_op=gt    range filter (see operators below)
GET /list/order?customer_id=c42          lookup column: index-backed
GET /list/order?status=a&tier=b&filterLogic=any   OR the filters (default: AND)
GET /list/order?pagesize=20&nexttoken=…  cursor pagination
GET /list/order?view=compact             apply a named view
GET /count/order?status=active           count, same filters -> { count, capped }
GET /list/order?q=urgent                 full-text search (Enterprise)
GET /list/order?facets=status            bucket counts (Enterprise)

How a list is answered

A request picks the most selective index it can — a lookup column, then the parent key, the owner scope, name, created_by/updated_by, or a time index chosen by sortby — and reads only that slice. Every other filter is applied to what that index returns, inside DynamoDB. With no index at all, the whole entity is read. Operators: eq (default), ne, gt, gte, lt, lte, between (with field_val2), begins, contains, notcontains, exists, notexists. Values are typed by the field: a number field compares as a number. An unknown operator, a bad value, or a filter on an encrypted or hidden field returns 400 — a filter is never silently ignored.

text
GET /list/order                          -> primary, all records of the entity
?customer_id=c42                         -> LookupIndex (a colType=lookup field)
?name=widget                             -> NameIndex, exact  (?name=wid&name_op=begins for a prefix)
?created_by=alice / ?updated_by=alice    -> CreatedByIndex / UpdatedByIndex
?sortby=updated_at&sortdir=desc          -> UpdatedAtIndex, newest first
?{parentKey}=val                         -> ParentIndex
?status=active                           -> entity partition + filter
?q=urgent  /  ?facets=status             -> OpenSearch (Enterprise), otherwise an empty result + note

Search is an Enterprise add-on

Full-text (?q=) and facets need an OpenSearch add-on on your instance — contact nostackai to enable it. Without it they return an empty list with a note, not a guess and not a slow scan. Filters, sorting, counts and lookups work everywhere.

Scoping a list

  • Owner-scoped — if the entity has an owner index, a plain GET /list/{entity} returns only the caller's records.
  • `?all=true` (or by=all) — skip the owner filter and return all of the org's records (needs a broad ACL).
  • `by=owner&userid=user456` — filter to a specific owner (needs a broad ACL). A bare ?owner=user456 is not the scoping mechanism.
  • Parent-scoped — ?{parentKey}={value} returns only children of that parent.
  • `by=category` — ?by=category&category_id=c8x2 returns every record in that category; add &subcategory_id=… (and &subsubcategory_id=…) to narrow. Uses the entity's category index; the parameter names are its category fields.
  • `?created_by=` / `?updated_by=` — records created / last updated by that user, newest first. On an owner-indexed entity, a caller without a broad ACL stays limited to their own records — like the parent key, the filter never widens access.

ACL applies to every query

The authorizer checks list{entity} before the handler runs, and the handler then enforces owner scope: on an entity with an owner index, a caller without a broad ACL only ever sees their own records — a lookup, parent or created_by filter narrows within that, it never widens it. Asking for another user's records with by=owner&userid= returns 403.

Pagination & counts

Pagination is by cursor: pass the nexttoken from the previous response for the next page (?pagesize= up to 500). A request reads at most about 2,000 records. If that limit stops a filtered search before the page is full, the response has scanLimited: true and a nexttoken to keep going — add an indexed filter (a lookup column, name, created_by) to narrow it. ?sortfield=<other field> sorts only the page returned and says sorted: "window". GET /count/{entity} takes the same filters and returns { count, capped }; capped: true means "at least this many". totalcount and facets appear only on an Enterprise search.