Skip to content

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

ts
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:

ts
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

FieldWhat Studio does
descriptionTooltip / detail panel
freshness: { p95MsBehindStream }Compares declared lag target to observed lag from telemetry

See also

MIT licensed.