Skip to main content

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"}