Projections
A projection is a read model: a view of your data, shaped for how it'll be read, kept current by the events as they happen. "Submissions by student," "orders awaiting fulfilment," "today's revenue" — each is a small state that the relevant events fold into.
It's the read side of the same split that gives you actors on the write side: the actor owns the rule and emits events when state changes; the projection listens and folds those events into a shape that's cheap to query. A projection is a pure fold — (state, event) => state, no side effects, no outside reads — so the runtime can rebuild it by replaying history.
Shape
import { defineProjection } from "@nwire/forge";
export const SubmissionsByStudent = defineProjection<{
byStudent: Record<string, SubmissionSummary[]>;
}>(
"submissions-by-student",
({ when }) => {
when(AnswerSubmittedEvent, (state, event) => ({
byStudent: {
...state.byStudent,
[event.studentId]: [
...(state.byStudent[event.studentId] ?? []),
{ id: event.submissionId, status: "submitted", submittedAt: event.submittedAt },
],
},
}));
when(SubmissionAutoGradedEvent, (state, event) => updateOne(state, event.studentId, event.submissionId, (s) => ({
...s,
status: "graded",
verdict: event.verdict,
})));
// …
},
{
description: "Index of submissions keyed by studentId — feeds Avi's history view.",
freshness: { p95MsBehindStream: 50 }, // Studio scores observed lag against this
initial: () => ({ byStudent: {} }),
},
);Where do they live?
In memory by default (InMemoryProjectionStore). For persistence:
import { createApp, defineRegistry } from "@nwire/app";
import { MongoProjectionStore } from "@nwire/mongo";
createApp({
appName: "orders",
registry,
plugins: [
...forgePlugins({ projectionStore: new MongoProjectionStore(client) }),
],
});Multi-tenancy
Projection state is partitioned by envelope.tenant. Each tenant gets its own folded state. No code change required — the framework partitions automatically.
Backfill / replay
Projections are pure functions over the event log. Replay a tenant's state by re-feeding events into runtime.publish after clearing the store. (Studio's "Replay" feature, coming.)
Studio-aware metadata
| Field | What Studio does |
|---|---|
description | Tooltip / detail panel |
freshness: { p95MsBehindStream } | Compares declared lag target to observed lag from telemetry |
See also
defineProjectionreference — full API.- Query — the read functions that serve a projection.
- Actor — the write side that emits what gets folded.