Skip to main content

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.

StrategyCompiles toWhen to use
from_collectionscan → filter → sort → pageFiltered and sorted list from a single collection
joinscan + scan → join → project → sort → pageList enriched with fields from another collection
aggregatescan → join → aggregate → sort → pageGrouped metrics and summary counts
timelinehistory_scan → order → group → accumulate → persist → pageEvent-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