expose config on an entity controls the entire VertzQL query surface — which fields appear in responses, which fields can be filtered or sorted, and which relations clients can include. It uses a fractal structure that mirrors the DB query API.
Two layers of field control
Field visibility operates at two levels:- Schema annotations —
.hidden()and.readOnly()on table columns control the schema-level defaults exposeconfig — narrows the API surface on top of schema defaults
expose can only narrow further — you cannot expose a .hidden() field through expose.
| Annotation | API response | Filterable | Sortable |
|---|---|---|---|
| (none) | Included | Yes | Yes |
.hidden() | Excluded | No | No |
.readOnly() | Included | Yes | Yes |
Basic usage
select is required
When expose is present, select must be provided. This enforces explicit control — if you opt into controlling your API surface, you control all of it:
select: {} for junction entities
An empty select is valid — the entity exposes no own fields, only relations:
Field exposure (select)
select declares which of the entity’s own fields appear in responses. Fields not listed are excluded:
Narrowing-only
select can only narrow what the schema already exposes. Hidden fields cannot be listed:
PublicColumnKeys<TTable>.
Filter allowlists (allowWhere)
allowWhere declares which fields clients can filter with where clauses. Uses object notation:
allowWhere, the server returns 400:
Defaults
| Config | Default | Meaning |
|---|---|---|
allowWhere omitted | No filtering allowed | Explicit opt-in |
No expose at all | All public fields filterable | Backwards compatible |
Sort allowlists (allowOrderBy)
allowOrderBy declares which fields can be sorted. Same pattern as allowWhere:
'asc' or 'desc'.
Relation exposure (include)
Relations are configured inside include. Each relation entry can be:
| Value | Meaning |
|---|---|
true | Expose with all non-hidden fields |
false | Do not expose — clients cannot include this relation |
{ select, allowWhere, allowOrderBy, maxLimit, include } | Fine-grained control |
Fractal structure
Theexpose config mirrors the DB query API at every level:
| DB Query | Entity Expose | Purpose |
|---|---|---|
select: { id: true } | select: { id: true } | Which fields are available |
where: { status: 'active' } | allowWhere: { status: true } | Which fields can be filtered |
orderBy: { createdAt: 'desc' } | allowOrderBy: { createdAt: true } | Which fields can be sorted |
include: { comments: { ... } } | include: { comments: { ... } } | Relation config (recursive) |
Nested relations
Relations can expose their own relations via nestedinclude:
Relation limits (maxLimit)
maxLimit caps how many related rows per parent:
limit: 200 with maxLimit: 50, the server silently clamps to 50.
Field-level access descriptors
Fields inselect, allowWhere, and allowOrderBy can use rules.* descriptors for conditional access:
null semantics
When a user doesn’t satisfy a descriptor, the field returns null — not omitted:
null is intentional:
- Signals “this field exists but you can’t see its value”
- The client always knows the field shape
- No runtime type surprises — the field is always in the object
Descriptor evaluation
Descriptors are evaluated once per request, not per row. They check user-level attributes (entitlements, roles, FVA), producing a static set of allowed/nulled fields for the entire response.Descriptor-guarded filters
When a user filters by a descriptor-guarded field they don’t have access to, the error says “field not filterable” — it doesn’t reveal that the field is access-controlled:Error responses
The server returns400 Bad Request with clear error messages for every rejection case.
Field not in allowWhere
Field not in allowOrderBy
Field not in select (client select query)
Relation not exposed
Relation field not in select
Nested relation error paths
Errors include the full relation path:Backwards compatibility
If you don’t provideexpose, the entity behaves as before:
- All non-hidden fields are included in responses
- All non-hidden fields are filterable and sortable
- No relations are exposed (you must opt in)
expose is always opt-in.
End-to-end example
1. Define the entity
2. Client query
3. Server validates
where: { status: 'todo' }—statusis inallowWhere✓orderBy: { createdAt: 'desc' }—createdAtis inallowOrderBy✓include.assignee: true—assigneeis exposed ✓include.comments.orderBy—createdAtis in comments’allowOrderBy✓include.comments.limit: 5— undermaxLimit: 20✓
4. Response
estimate is null because the user lacks the pm:view-estimates entitlement.
Next steps
Schema
Column types, annotations, and how
.hidden() and .readOnly() affect the API surface.Entities
Entity definitions, access rules, hooks, and custom actions.
Queries
Typed CRUD operations, filter operators, and the DB query API.
Authentication
Access rules,
rules.* descriptors, and defineAccess().