GET /admin/tenants/count, GET /subjects/count, GET…
GET /admin/tenants/count, GET /subjects/count, GET /clients/count, GET /roles/count, GET /groups/count and GET /scopes/count
How many rows a listing would page through, without paging through them.
Each count takes its list's own parameters minus ?cursor= and ?limit=,
under the same rules — a search is one field at a time, every filter is
ANDed, anything else is refused with 400 naming it — and requires its
list's own capability: manage-tenants for the tenant collection, which
carries no {tenant} segment for the same reason GET /admin/tenants
does; view-users for subjects, which manage-users also reaches;
manage-clients for clients; and manage-tenant for roles, groups and
scopes. count is a reserved tenant name, so /admin/tenants/count never
names a tenant, and a subject or client id is a uuid, so /count never
names one either.
The answer is {"count": n, "capped": false}, or {"count": 10000, "capped": true} once more than ten thousand rows match: counting stops one row past that
ceiling. A count reads the rows its list would page through, in its list's
order, from the same index — never a pass over the table and never another
tenant's rows. The one read beyond that: a searched subjects count whose matches
fall under the ceiling may read the tenant's whole subject index to join it,
when the planner costs that cheaper than one probe per match (an integration
test holds both plans). A capped count says only that there are more; it is
never an estimate. No capped response is shown here, since nothing here holds
ten thousand of anything — an integration test covers it with a lowered ceiling.
As ada-whoami, after its odudu service was rebuilt from this branch, with
the roles, groups, subjects and clients the sections above created in its
demo. The tenant collection first, then the list the second count agrees with:
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/count"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/count?name=demo"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants?name=demo"
{"count":8,"capped":false}
{"count":1,"capped":false}
{"items":[{"id":"01a0db22-1c32-7d17-b351-697d7911033c","name":"demo","display_name":null,"enabled":true,"created_at":"2026-09-26T00:34:00.885Z"}]}
Subjects, unfiltered and then under ?username=ADA — the three the search
under GET /subjects returns — and under ?enabled=false, which nothing
in demo is:
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/subjects/count"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/subjects/count?username=ADA"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/subjects/count?enabled=false"
{"count":7,"capped":false}
{"count":3,"capped":false}
{"count":0,"capped":false}
Clients, roles, groups and scopes, each unfiltered or under the searches their own sections ran:
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/clients/count"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/clients/count?type=public"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/roles/count?name=billing"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/roles/count?name=billing&client=tenant"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/groups/count?name=eng"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/scopes/count"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/admin/tenants/demo/scopes/count?name=o"
{"count":5,"capped":false}
{"count":1,"capped":false}
{"count":3,"capped":false}
{"count":2,"capped":false}
{"count":2,"capped":false}
{"count":8,"capped":false}
{"count":2,"capped":false}
A page control is not a count parameter, and two search fields are refused as
the list refuses them. The first two refusals are the generated schema's, hence
their generic title; the third is the handler's. In demo, since none of them
reads a row:
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3080/admin/tenants/demo/subjects/count?limit=5"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3080/admin/tenants/demo/subjects/count?cursor=x"
curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3080/admin/tenants/demo/subjects/count?username=a&email=b"
{"type":"about:blank","title":"Error","status":400,"detail":"querystring must NOT have additional properties: limit","errors":[{"path":"limit","message":"must NOT have additional properties"}],"instance":"01a0ea61-845b-74ab-9020-373d1a6ada67"}
{"type":"about:blank","title":"Error","status":400,"detail":"querystring must NOT have additional properties: cursor","errors":[{"path":"cursor","message":"must NOT have additional properties"}],"instance":"01a0ea61-846d-7b1b-afe7-c61051d5153f"}
{"type":"about:blank","title":"Bad Request","status":400,"detail":"search one field at a time: username or email, not both","errors":[{"path":"email","message":"search one field at a time: username or email, not both"}],"instance":"01a0ea61-847a-7970-bb2c-77e99c2bf5e8"}