@fabricorg/platform
v1.1.0
Published
Generic, vertical-agnostic runtime for ontology-based business applications. Provides actions, modules, events, policies, state machines, adapters, displays, projections, privacy, and evidence contracts. Zero runtime dependencies; portable across any data
Downloads
15,083
Readme
@fabricorg/platform
A portable runtime for ontology-based, agent-native business applications. Zero dependencies. Bring your own database client.
npm install @fabricorg/platformHello, action
import { z } from "zod";
import type { ActionDefinition, ActionContext } from "@fabricorg/platform/actions";
const greet: ActionDefinition<unknown> = {
id: "demo.greet",
schema: z.object({ name: z.string() }),
handler: async (ctx: ActionContext<unknown>, params: { name: string }) => {
return { greeting: `Hello, ${params.name}!` };
},
};That's the smallest unit. From here, you compose actions into a FabricModule<TDb>, register the module at startup, and call invokeAction("demo.greet", ...) from any of three call paths (authenticated user, signed token, or system/agent). Every call passes through the same pipeline: policy → state machine → handler → adapters → events → projections.
For a complete walkthrough see https://platform.fabric.pro/docs/getting-started/quickstart.
Why use this
You're building a governed business application — not a CRUD app. You need:
- A single, auditable entry point for every mutation
- Policies that gate every change before it happens
- Events as evidence (not retrofitted logs)
- Agents and humans treated identically by policy
- A vertical-specific ontology layered on a portable runtime
That's what this package gives you. Nothing more.
What's in the box
| Subpath | What you get |
|---|---|
| @fabricorg/platform | Re-exports everything below |
| @fabricorg/platform/actions | ActionDefinition, ActionContext, SagaImplementation |
| @fabricorg/platform/policies | PolicyEvaluator, PolicyContext, code/data/hybrid evaluation |
| @fabricorg/platform/state-machines | StateMachineDefinition, validateTransition |
| @fabricorg/platform/events | AssetEventEnvelope, EventTypeRegistry |
| @fabricorg/platform/projections | Replay/snapshot framework |
| @fabricorg/platform/adapters | AdapterRegistry, retry + circuit-breaker |
| @fabricorg/platform/modules | FabricModule<TDb>, createModuleRegistry |
| @fabricorg/platform/manifest | exportModuleManifest, exportModuleManifestBundle, ModuleManifest |
| @fabricorg/platform/ids | createFabricId, FABRIC_ID_PREFIXES |
| @fabricorg/platform/privacy | Redaction + data classification |
| @fabricorg/platform/evidence | Compliance evidence packets |
| @fabricorg/platform/governance | External footprints, delegated principals, obligations, attestations, and reconciliation |
| @fabricorg/platform/objects | Shared object-type contracts |
| @fabricorg/platform/testing | In-memory adapter registry, fake ActionContext, deterministic IDs |
Modules may publish versioned ViewContracts. replayView filters declared event schema versions and
requires an explicit upgrade function before resuming an older compatible snapshot; manifests serialize
the logical shape and metadata but never executable functions or initial state. Physical stores remain
adapter concerns.
Composing a vertical
Verticals declare a FabricModule<TDb> that registers their actions, policies, state machines, events, and adapters with the platform:
import type { FabricModule } from "@fabricorg/platform/modules";
import { createModuleRegistry } from "@fabricorg/platform/modules";
const myModule: FabricModule<MyDbClient> = {
namespace: "demo",
version: "1.0.0",
objectTypes: ["Widget"],
actions: [greet, /* ... */],
policies: [/* ... */],
stateMachines: [/* ... */],
};
// At app startup:
const registry = createModuleRegistry([myModule]);The platform never auto-loads a module. The host app composes an instance-scoped registry and injects it into its hosts. Experience roles, slots, action intents, and display rendering live in @fabricorg/experience-contracts, carried through the namespaced fabric.experience/v1 extension.
Exporting a module manifest
Downstream tooling — semantics exporters, docs generators, tool generators — often needs a module's ontology as data rather than live code. @fabricorg/platform/manifest exports a stable, JSON-serializable, versioned description of any FabricModule:
import { exportModuleManifest } from "@fabricorg/platform/manifest";
const manifest = exportModuleManifest(myModule);
const json = JSON.stringify(manifest, null, 2);exportModuleManifest is a pure function of the module object: it does not touch the registries and the module does not need to be registered first. Declaration order is preserved everywhere, so the same module always produces byte-identical JSON. Function fields (handler, policy evaluate, transition guard, dynamic targetState, adapter getInput) are never serialized — the manifest contains definitions only, never raw parameter values, secrets, or runtime data. Manifest v2 object registrations may include PortableJsonSchema, qualified relations, namespaced semantic annotations, and enforcement-owned invariants. Actions whose validators expose toJSONSchema() include parameterSchema and optional resultSchema definitions. normalizeModuleManifest and normalizeModuleManifestBundle let v2 readers accept v1 input without promising down-conversion.
An entity schema does not validate arbitrary writes through TDb. Persisted-record validation requires an explicit Host/database adapter. When an action declares resultSchema, Platform Host validates the public result after the handler and before events or adapters; failure rolls back domain writes only when the Host store has a transaction provider that includes them.
The action lifecycle
Every invokeAction(id, params) call goes through:
evaluatePolicies— every policy registered for the action runs (code/data/hybrid). A singleblockhalts the invocation. Policies may returnguidance(factsUsed+ typedcorrectiveActions) so hosts can show exactly why a block happened and what the operator can do next. Corrective actions declarekind: "invoke_action" | "navigate" | "manual"so UIs do not guess whether a remediation is executable.- State-machine validation — if the action's target entity is bound to a state machine, the requested transition is validated before the handler runs.
- Handler —
actionDef.handler(ctx, params)runs inside a singleTDbtransaction. Handler parses its own input via itsschema(single source of truth). - Adapters — for each
actionDef.adapterSteps, the registered implementation runs viaexecuteWithAdapterRetry(idempotent retry, exponential backoff, circuit breaker). - Events — domain events are appended; projections derive read models on next replay.
- Completion — invocation status is marked, telemetry emitted.
validateActionParameters is also exposed for preview/dry-run paths but is not part of the execute pipeline.
Saga actions
Multi-step orchestrations declare kind: "saga" on the ActionDefinition and ship a SagaImplementation:
import type { SagaImplementation } from "@fabricorg/platform/actions";
export const ingestSaga: SagaImplementation = async (ctx, runChild) => {
const step1 = await runChild({
suffix: "step-1",
actionId: "demo.receive_payload",
parameters: { /* ... */ },
});
// orchestrate further child actions...
return {
resultData: { /* ... */ },
event: { eventType: "...", subjectType: "...", subjectId: "...", payload: {} },
};
};Your worker maps action IDs to implementations. The platform dispatches generically — no platform code references vertical action IDs.
Portability boundary
The repository Boundary Guard scans source and packed artifacts and fails CI if any of these leak in:
- Concrete database/ORM imports — the literal
Prisma, the field nameprisma:, or the type defaultTDb = any - Vertical vocabulary from the governed denylist
- "Register default" hooks (
registerDefaultPolicies, etc.) — the platform never auto-loads anything - Module-scope mutable registries, import-time environment reads, or unapproved infrastructure imports
This is what keeps the platform portable across verticals and consumers. If you fork or add new patterns specific to your domain, extend the list in boundary.test.ts.
Compatibility
- Runtime: Node 20+
- Module formats: ESM and CommonJS (dual format from v0.2.0+)
- TypeScript: 5.0+,
moduleResolution: nodeornode16/nodenext
Versioning
0.x while contracts evolve. 1.0 only after at least two materially different verticals validate the API.
See CHANGELOG.md for release history.
Documentation
Full reference docs at https://platform.fabric.pro
License
MIT
