A list endpoint should derive its candidate set from the same authorization semantics as GET /objects/{id}. Fetching an entire collection and checking rows one at a time is slow, leaks through pagination, and eventually disagrees with direct access. Choose query-time filtering whenever the policy can be compiled safely; otherwise use a bounded candidate set and make incomplete results explicit.
Define one visibility predicate
Write can_view(subject, object, context) once as the contract. A direct get returns the object only when that predicate is true. A list returns exactly the objects for which it is true, under a documented consistency model. Search, exports, autocomplete, totals, and activity feeds are all list surfaces and need the same rule.
For relationship-based access, the authorization system can return object identifiers or constrain candidates. The OpenFGA search-with-permissions guide describes three patterns: search then check candidates, obtain authorized object IDs before querying, or combine authorization with application-specific filtering. Their costs depend on candidate size, indexability, and consistency.
For attribute policies, partial evaluation can turn a policy into database conditions. OPA’s data filtering documentation explains how policy can produce filters for external data. Treat the generated condition as code: constrain its operators, parameterize values, test semantic equivalence, and fail closed when the policy cannot be translated. A translation that drops one clause widens access.
Choose the plan from candidate size
Authorization-first works when the permitted ID set is selective and can be joined efficiently. Application-query-first followed by checks works when the business query is selective, such as one project with twenty records. A database-native predicate works best when tenant, owner, classification, and membership are already queryable with appropriate indexes. Search-index filtering is useful for full-text results, but only if authorization facts reach the index within an acceptable revocation delay.
Avoid unlimited post-filtering. Suppose a page requests 20 items, but 18 of the first 20 candidates are forbidden. Returning two items with has_more=false is false; fetching more until the page fills can require an unbounded scan. Use an opaque cursor over the underlying stable ordering and continue scanning within a fixed work budget. Return a continuation cursor when more candidates may exist, even if the visible page is sparse.
Counts are authorization decisions too. total: 8,421 can reveal hidden objects, while a filtered exact count may be expensive. Offer a count only when computed from the same authorized set. Otherwise omit it, expose a documented lower bound, or calculate asynchronously. Never mix an authorized page with an unfiltered total.
Keep list and get consistent under change
Pass the same subject, tenant, action, and relevant context to both paths. Include authorization model or relationship revision in internal telemetry. If the list uses a stale index and direct get uses fresh policy, decide which behavior users see: remove an item that fails a final check, or use a bounded consistency token supported by the authorization system. Do not show sensitive fields while relying on a later click to enforce access.
Test revoked membership during pagination, newly granted access, policy updates, duplicate sort keys, and objects deleted between pages. Compare list IDs against direct checks for generated subjects and objects. This property test catches drift better than separate happy-path tests.
Sorting is part of the disclosure boundary. A hidden object can affect the rank or cursor of visible objects even when its fields are omitted. Sort the authorized result set where practical, make the cursor opaque, and include tenant plus policy context in its server-side interpretation.
The next step is to instrument one costly list endpoint: candidate count, authorization calls, visible count, scan depth, decision revision, and latency. Use a week of data to choose authorization-first, query-first, or compiled filtering. Review by 2026-12-05 or when the policy model changes.
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗