Projections
A projection is a read-optimized, derived view of one or more collections. Projections are the read side of the domain. They never mutate state and are not callable directly — they are always exposed externally through a named query.
The four strategies
Projections are declared with a strategy field that determines how the projection compiles and executes. The four strategies form a progression from simple to complex.
| Strategy | Compiles to | When to use |
|---|---|---|
from_collection | scan → filter → sort → page | Filtered and sorted list from a single collection |
join | scan + scan → join → project → sort → page | List enriched with fields from another collection |
aggregate | scan → join → aggregate → sort → page | Grouped metrics and summary counts |
timeline | history_scan → order → group → accumulate → persist → page | Event-sourced read model built from signal history |
The tiers are not a ranking. from_collection is not inferior to timeline. Each strategy is the right choice for its use case.
Projections are not public
A projection is never callable directly from outside the application. It becomes publicly accessible only when explicitly exposed as a named query.
queries:
- id: list_messages
access:
roles: [authenticated]
in:
q: text
page: int
page_size: int
returns:
projection: all_messages
The query is the public contract. The projection is the implementation. They are deliberately separate — the same projection can back multiple queries with different input shapes, and a projection can exist without being exposed at all (for use by views only).
Common fields across strategies
All strategies support sort and pagination. Most support where for row-level filtering.
Sorting
sort:
by_arg: sort_by # input arg that selects the sort field
order_arg: sort_order # input arg that selects asc or desc
allowed: [created_at, message]
default_by: created_at
default_order: desc
by_arg and order_arg are optional. If omitted, sorting is fixed to default_by and default_order.
Pagination
pagination:
mode: page
page_arg: page
page_size_arg: page_size
default_page_size: 20
max_page_size: 100
mode: page is the only supported pagination mode. page_arg and page_size_arg name the input arguments that control which page and page size the caller requests.
Filtering with where
where:
"==":
- var: row.user_id
- var: principal.subject
where accepts a single $expr rule. The row.* namespace refers to fields on the primary collection row. The principal.* namespace exposes the authenticated caller's identity.
The $expr DSL in projections
Projections use $expr in where clauses, join on conditions, and timeline when handlers. The same operator set applies everywhere.
where:
and:
- "==":
- var: row.status
- approved
- "!==":
- var: row.deleted_at
- null
What's next
- from_collection: filtering and sorting a single collection
- join: enriching rows with fields from another collection
- aggregate: grouped metrics and summary counts
- timeline: event-sourced read models from signal history