TechFabricTechFabricPlatform
Platform referenceAction pipeline

Projection creation from events

How read models, dashboards, and analytics views are derived from the AssetEvent log.

Projections are derived views built by replaying events through a reducer. The platform's projection engine (packages/platform/projections/index.ts) provides the replay primitives; verticals define what the projections mean. @fabricorg/projection-host turns those primitives into an authorized, tenant-scoped query seam with snapshot freshness and offline controls.

What a projection is

function applyEvent(state: ProjectionState, event: AssetEvent): ProjectionState

That's it. A projection is:

  1. An initial state.
  2. A reducer.
  3. Optionally, a snapshot to skip past prior events.

Replay this over the event stream → you get the projection's current state.

What replayEvents does

The platform ships a replayEvents helper that handles the hard parts:

const result = await replayEvents({
  events,                   // iterable or async iterable
  scope,                    // { tenantId, spaceId, subjectType?, subjectId? }
  initialState,
  snapshot,                 // optional ReplaySnapshot
  applyEvent,
});
var result = await ReplayEventsAsync(new ReplayEventsRequest
{
    Events = events,                   // IEnumerable<AssetEvent> or IAsyncEnumerable<AssetEvent>
    Scope = scope,                     // { TenantId, SpaceId, SubjectType?, SubjectId? }
    InitialState = initialState,
    Snapshot = snapshot,               // optional IReplaySnapshot
    ApplyEvent = applyEvent,
});

Behind the scenes:

  • Scope filtering — drops events outside tenantId / spaceId / subject.
  • Sorting — subject-scoped replay sorts by explicit subjectSequence when supplied, otherwise by ledger sequence. Global replay sorts by recordedAt with deterministic tiebreakers.
  • Deduplication — duplicate ids are dropped, with a duplicate_event warning.
  • Gap detectionmissing_sequence warnings require an explicit contiguous subjectSequence; gaps in a filtered global ledger are valid.
  • Cursor resume — if the snapshot's eventCursor matches an event in the stream, replay starts after it. If not, a cursor_not_found warning fires.
  • Subject-scope completeness — passing subjectType without subjectId (or vice versa) emits subject_scope_incomplete.

Snapshots

A snapshot captures { snapshotData, eventCursor, eventSequence }. Store one periodically; on restart, replay only the events after the cursor.

interface ReplaySnapshot<State> {
  snapshotData: State;
  eventCursor?: string | null;
  eventSequence?: number | null;
}
public interface IReplaySnapshot<TState>
{
    TState SnapshotData { get; set; }
    string? EventCursor { get; set; }
    int? EventSequence { get; set; }
}

Snapshots are an optimization, not a correctness mechanism. A cold rebuild from event 1 must yield the same state.

Idempotency on the consumer side

The replay engine assumes the reducer is deterministic and side-effect-free with respect to state. If a projection writes to a database, it must:

  • Use the event's id as an idempotency key (insert-on-conflict-do-nothing).
  • Tolerate replays — re-running the reducer on the same event must converge.

The platform makes the events deterministic; the reducer must do its part.

Diagram

stream AssetEvent log replayEvents Snapshot initialState applyEvent reducer ReplayEventsResult state eventCursor warnings

Common projection shapes

Use caseScopeReducer behavior
Single-subject viewsubject-scopedMaintain a per-subject record (e.g. an plan's status).
Aggregate countersglobal, filteredIncrement counters keyed by (tenantId, spaceId, …).
Audit queryglobal, time-windowedAppend-only — every event is a row in the audit table.
Search indexglobal, derivedCompute search docs from event payloads.

What projections must not do

  • Don't mutate event payloads. Treat them as immutable inputs.
  • Don't infer missing subject events from gaps in sequence. It is a tenant/space ledger cursor. Use recordedAt plus deterministic tiebreakers for global replay, and provide subjectSequence only when the source guarantees subject-local continuity.
  • Don't write back into the event log. Projections are downstream. Corrections happen by emitting new events from actions, never by editing past ones.

See also

On this page