Skip to content

Views

Views and projections

A view is a read model a capability projects from facts. Views are real tables generated from their row shapes, maintained by projections in the same exactly-once delivery as reactions, and rebuilt from the outbox when their shape changes.

export default capability({
name: 'order-summary',
intent: '...',
views: {
// Built from facts only, keyed by order; the index serves "my orders, newest id first" (eq customerId, order by orderId).
'customer-orders': { row: t.object({ orderId: t.string, customerId: t.string, status: t.enum('placed', 'cancelled'), totalCents: t.int }), key: 'orderId', indexes: [['customerId', 'orderId']] },
},
})
.project('OrderPlaced', {
needs: t.object({ orderId: t.string, customerId: t.string, totalCents: t.int }),
rows: (f, v) => [v.upsert('customer-orders', f.orderId, { orderId: f.orderId, customerId: f.customerId, status: 'placed', totalCents: f.totalCents })],
examples: [{ name: 'lists the order for its customer', fact: { orderId: 'o1', customerId: 'c1', totalCents: 900 }, expect: { rows: [{ op: 'upsert', view: 'customer-orders', key: 'o1', row: { customerId: 'c1', status: 'placed' } }] } }],
})
.project('OrderCancelled', {
needs: t.object({ orderId: t.string }),
rows: (f, v) => [v.patch('customer-orders', f.orderId, { status: 'cancelled' })],
examples: [{ name: 'marks it cancelled', fact: { orderId: 'o1' }, expect: { rows: [{ op: 'patch', key: 'o1', row: { status: 'cancelled' } }] } }],
});
Concept Rule
View views: { name: { row, key, indexes? } }. Names are global and become table and column names (ATLAS020): a view name has at most 40 chars matching [A-Za-z][A-Za-z0-9_-]*, a row field at most 63 matching [A-Za-z][A-Za-z0-9_]*. Built only from facts, so a view never holds sensitive data (ATLAS019). Disposable: runtime.rebuild(view) or atlas rebuild.
Projection .project(fact, { needs, rows(fact, v) }), pure and read-free; examples expect rows (partial match, in order).
Row ops v.upsert (the key must equal the row’s key field), v.patch and v.inc (no-op on a missing row), v.remove.
Delivery By the capability’s consumer with its reactions; rows commit with the cursor advance: exactly once, in order.
Counters v.inc never reads, so counters have no contention: one record per like or follow, and the count is a projection.

A rebuild fills an empty shadow table by replaying the outbox up to the owning consumer’s cursor, then swaps it in atomically with that cursor, so no fact is lost or applied twice and queries keep reading the old table until the swap.

  • Views that are new or changed incompatibly are rebuilt automatically before delivery (drain / start); while stale, live rows for them are skipped.
  • Additive changes (a new optional field, a dropped field, a new index) apply in place.
  • A newer rebuild supersedes an older one (generation check), so concurrent boots are safe; a crashed rebuild restarts from scratch on the next boot.
  • Force one with atlas rebuild <view> --db <url>.

A view is updated after the commit, like any reaction. A caller that needs its own write right away waits with await: 'views' or reads the record through a record query. See Queries.

Each view is one real table, atlas_view_<name>: string, enum, int, number and boolean fields become typed columns (text with COLLATE "C", bigint, double precision, boolean), string arrays text[] with a GIN index, anything else jsonb. Declared indexes become btree indexes. See the Postgres guide.