FabricFabricPlatform
Platform referenceReference

ProjectionHost

Authorized, tenant-scoped execution and delivery of versioned Fabric view projections.

ProjectionHost

@fabricorg/projection-host is the vertical-neutral query counterpart to the governed mutation Host. It executes registered ViewContracts through one small interface:

interface ProjectionHost {
  project<State = unknown>(query: ProjectionQuery): Promise<ProjectionResult<State>>;
}

Behind that interface the Host authorizes the read, validates scope, selects a tenant-scoped snapshot, enforces freshness and offline rules, incrementally reads canonical events, replays the view, refuses unsafe checkpoints, and persists the new snapshot. Applications do not reproduce those rules in each GraphQL resolver, SDUI composer, API route, or agent tool.

Create a Host

import {
  createProjectionHost,
  PostgresProjectionEventSource,
  PostgresProjectionSnapshotStore,
} from "@fabricorg/projection-host";

const projections = createProjectionHost({
  views: module.views ?? [],
  eventSource: new PostgresProjectionEventSource(sql),
  snapshotStore: new PostgresProjectionSnapshotStore(sql),
  authorize: async ({ actor, scope, view }) =>
    policy.canRead(actor, scope, view)
      ? { allowed: true, decisionId: "opaque-decision-reference" }
      : { allowed: false, reason: "read denied" },
  onQuery: recordQueryEvidence,
});

The PostgreSQL adapters use a narrow SQL client, read the canonical Platform Host event ledger with tenant-scoped cursor constraints, and save snapshots with an atomic compare-and-swap update. The included in-memory adapters exist for local execution and interface-level tests. Event adapters may return events in any order; the Host applies the deterministic ordering and version rules from replayView. If an adapter can no longer honor a snapshot cursor, it throws ProjectionCursorUnavailableError and the Host performs a cold rebuild.

Snapshot records persist both the global cursor and subjectSequences, a per-subject high-water map. Values use explicit subjectSequence when an event source supplies it; otherwise they preserve the latest global ledger cursor seen for that subject without claiming contiguity. Incremental replay uses those checkpoints to avoid reapplying an event already represented by durable state and detects gaps only when subject-local continuity was explicitly declared. Legacy snapshots that lack these checkpoints are rebuilt rather than trusted for incremental replay. An event source may declare that its stream begins after a snapshot only when the snapshot has a durable event cursor; the replay API fails closed if that cursor is absent.

Query contract

const result = await projections.project({
  view: { name: "module/summary", version: "1" },
  scope: { tenantId, spaceId, subjectType, subjectId },
  actor: { id: principal.id, type: principal.type },
  parameters: { region: "east" }, // required when the view declares parameterSchema
  consistency: "current", // current | bounded-stale | offline
  maximumAgeSeconds: 15,  // may tighten, never relax, the view declaration
  minimumCheckpoint: { eventSequence: writeEventSequence },
  checkpointTimeoutMs: 2_000,
});

Actor, tenant, and space values must be derived from authenticated server context. A query argument, path, host header, SDUI document, or cached record is not an authorization oracle. Authorization runs before either the snapshot store or event source is touched.

Views can declare a portable parameterSchema. ProjectionHost validates required parameters before authorization, passes them to authorization and event-source adapters, and adds a canonical SHA-256 parameter identity to the snapshot key. Parameter values are not persisted in snapshot records or query audit records. The identity prevents two variants of one view from sharing state. Missing, extra, or invalid parameters return ProjectionParameterValidationError.

minimumCheckpoint is a lower bound on the tenant/space event ledger. When the requested position is newer than the cached projection, the event source must implement waitForCheckpoint; the PostgreSQL source polls asset_events within checkpointTimeoutMs (five seconds by default). Checkpoint polling starts only after authorization and always includes the requested tenant and space. If the ledger does not reach the requested position in time, the Host throws ProjectionCheckpointTimeoutError with code PROJECTION_CHECKPOINT_TIMEOUT rather than returning data as if it were current. Checkpoint waits are not available with offline consistency.

onQuery receives an audit record for allowed and denied attempts. Denied records contain the reason but no projection state. Successful calls return isolated state graphs, including calls coalesced by the same in-process refresh, so one resolver cannot mutate another caller's result.

Consistency and offline behavior

ModeBehavior
currentResume from a compatible snapshot, read later events, replay, and save a new snapshot.
bounded-staleReturn a snapshot only while it is inside the effective freshness bound; otherwise refresh it.
offlineNever touch the event source. The view must declare offlineUsable: true, declare maximumAgeSeconds, and have a fresh snapshot.

The effective freshness bound is the stricter of the view declaration and the caller request. A caller cannot extend a view's declared lifetime. Snapshot keys always bind view name and version, tenant, space, optional subject identity, and the canonical parameter identity. Views that change their state shape should list compatible older snapshot versions and provide upgradeSnapshot(data, fromVersion); the Host persists the upcast state at the current snapshot version and cold-rebuilds incompatible snapshots.

Integrity failures

The mutation Host can retry work; ProjectionHost must not manufacture a plausible read model from an incomplete stream. It refuses to save or return a refreshed projection when replay reports:

  • a missing explicitly declared subject sequence;
  • an unsupported event schema version;
  • an incomplete subject scope; or
  • a cursor mismatch that the event-source adapter did not explicitly convert into a cold rebuild.

These failures use PROJECTION_INTEGRITY_FAILED. A snapshot-store record whose identity differs from the requested tenant-scoped key fails with PROJECTION_SNAPSHOT_SCOPE_MISMATCH; it is never returned. Concurrent refreshes for the same full snapshot key are coalesced within a Host instance. Across Host instances, ProjectionSnapshotStore.save is a compare-and-swap operation: an adapter must return conflict when its existing checkpoint no longer matches the Host's expected record, preventing a stale writer from regressing a newer snapshot.

Generated GraphQL seam

@fabricorg/gen-graphql emits exact viewIds and a resolveProjection helper. Query resolvers pass server-derived actor and scope context to that helper, which calls ProjectionHost.project. Generated queries never access a projection table directly. Mutations continue to use createGovernedActionHost().submitAction().

What this package does not own

ProjectionHost contains no layout, components, themes, renderer behavior, SDUI documents, commerce assumptions, or other vertical vocabulary. A vertical defines view meaning and supplies adapters; Platform owns the safe execution mechanics. Screen composition and delivery remain an experience-layer responsibility outside @fabricorg/platform core.

On this page