FabricFabricPlatform
Getting started

Quickstart

Install @fabricorg/platform, declare a module, register it, and invoke your first action — end to end.

This walks through a complete, runnable example: define a vertical module, register it with the platform, invoke an action through the policy → state machine → handler → event → projection pipeline, and project events into a read model.

The full source for this walkthrough is in examples/minimal-vertical — clone the repo and run pnpm --filter @fabricorg/example-minimal-vertical start to see it in action.

1. Install

pnpm add @fabricorg/platform
# or
npm install @fabricorg/platform

The platform has zero runtime dependencies. You bring your own database client and bind it via the TDb type parameter.

Requires Node 20+. Supports both ESM (import) and CommonJS (require).

dotnet add package FabricOrg.Platform.Abstractions
dotnet add package FabricOrg.Platform
dotnet add package FabricOrg.Platform.DependencyInjection   # optional, for DI wiring

The platform targets .NET Standard 2.1 and .NET 8.0. You bring your own database client and bind it via the TDb type parameter or DI.

2. Declare an action

An ActionDefinition is a single mutation entry point — schema, handler, policies it must clear, events it emits, and (optionally) the state-machine transition it represents.

import type { ActionContext, ActionDefinition } from "@fabricorg/platform/actions";

interface SubmitOrderParams {
  orderId: string;
  total: number;
}

const submitOrderAction: ActionDefinition = {
  actionId: "order.submit",
  namespace: "order",
  version: 1,
  schema: {
    parse: (input) => input as SubmitOrderParams,
    safeParse: (input) => ({ success: true, data: input as SubmitOrderParams }),
  },
  policies: ["order.large_order_requires_approval.v1"],
  emitsEvents: ["OrderSubmitted"],
  idempotent: false,
  mutatesDomain: true,
  stateMachine: {
    entityType: "Order",
    targetState: "submitted",
    getEntityId: (params) => (params as SubmitOrderParams).orderId,
  },
  handler: async (_ctx: ActionContext, params) => {
    const { orderId } = params as SubmitOrderParams;
    // Real verticals would write through ctx.db here.
    return { success: true, data: { orderId, status: "submitted" } };
  },
};
using FabricOrg.Platform.Actions;

public record SubmitOrderParams(string OrderId, decimal Total);

public class SubmitOrderAction : ActionDefinition<SubmitOrderParams>
{
    public SubmitOrderAction()
    {
        ActionId = new ActionId("order", "submit");
        Namespace = "order";
        Version = 1;
        Policies = [new PolicyId("order", "large_order_requires_approval", 1)];
        EmitsEvents = ["OrderSubmitted"];
        Idempotent = false;
        MutatesDomain = true;
        StateMachine = new StateMachineBinding
        {
            EntityType = "Order",
            TargetState = "submitted",
            GetEntityId = (params) => params.OrderId,
        };
    }

    public override async Task<ActionResult> HandlerAsync(
        IActionContext ctx,
        SubmitOrderParams params,
        CancellationToken ct)
    {
        // Real verticals would write through ctx.Db here.
        return ActionResult.Success(new { params.OrderId, Status = "submitted" });
    }
}

For real schemas, use Zod (TypeScript) or FluentValidation (C#) — both implement the SchemaValidator interface natively.

3. Declare a state machine

import type { StateMachineDefinition } from "@fabricorg/platform/state-machines";

const orderStateMachine: StateMachineDefinition = {
  entityType: "Order",
  states: {
    draft: { id: "draft", label: "Draft", stateClass: "initial" },
    submitted: { id: "submitted", label: "Submitted", stateClass: "active" },
    delivered: { id: "delivered", label: "Delivered", stateClass: "terminal" },
  },
  transitions: [
    { from: "draft", to: "submitted", causedByAction: "order.submit" },
    { from: "submitted", to: "delivered", causedByAction: "order.deliver" },
  ],
};
using FabricOrg.Platform.StateMachines;

var orderStateMachine = new StateMachineDefinition
{
    EntityType = "Order",
    States = new Dictionary<string, StateDefinition>
    {
        ["draft"] = new StateDefinition("draft", "Draft", StateClass.Initial),
        ["submitted"] = new StateDefinition("submitted", "Submitted", StateClass.Active),
        ["delivered"] = new StateDefinition("delivered", "Delivered", StateClass.Terminal),
    },
    Transitions = new List<StateTransition>
    {
        new StateTransition("draft", "submitted", new ActionId("order", "submit")),
        new StateTransition("submitted", "delivered", new ActionId("order", "deliver")),
    }
};

4. Declare a policy

import type { PolicyContext, PolicyEvaluator } from "@fabricorg/platform/policies";

const largeOrderPolicy: PolicyEvaluator = {
  policyId: "order.large_order_requires_approval.v1",
  version: 1,
  evaluate: async (ctx: PolicyContext) => {
    const { total } = ctx.parameters as { total: number };
    if (total > 10_000) {
      return {
        policyId: "order.large_order_requires_approval.v1",
        policyVersion: 1,
        result: "block",
        reason: "over $10,000 requires approval",
      };
    }
    return {
      policyId: "order.large_order_requires_approval.v1",
      policyVersion: 1,
      result: "pass",
    };
  },
};
using FabricOrg.Platform.Policies;

public class LargeOrderPolicy : IPolicyEvaluator
{
    public PolicyId PolicyId => new PolicyId("order", "large_order_requires_approval", 1);
    public int Version => 1;

    public async Task<PolicyEvaluationResult> EvaluateAsync(IPolicyContext ctx, CancellationToken ct)
    {
        var total = (decimal)(ctx.Parameters["total"] ?? 0);
        if (total > 10_000)
        {
            return new PolicyEvaluationResult(
                new PolicyId("order", "large_order_requires_approval", 1),
                1,
                PolicyOutcome.Block,
                reason: "over $10,000 requires approval");
        }
        return new PolicyEvaluationResult(
            new PolicyId("order", "large_order_requires_approval", 1),
            1,
            PolicyOutcome.Pass);
    }
}

5. Compose into a module

A FabricModule is the manifest your vertical ships. It declares object types, event types, actions, policies, and state machines — and the platform wires them all into its registries when you call registerFabricModules.

import type { FabricModule } from "@fabricorg/platform/modules";

const orderModule: FabricModule = {
  namespace: "order",
  version: "1.0.0",
  objectTypes: ["Order"],
  subjectTypes: ["Order"],
  eventTypes: ["OrderSubmitted"],
  actions: [submitOrderAction],
  policies: [largeOrderPolicy],
  stateMachines: [orderStateMachine],
};
using FabricOrg.Platform.Modules;

var orderModule = new FabricModule
{
    Namespace = "order",
    Version = "1.0.0",
    ObjectTypes = ["Order"],
    SubjectTypes = ["Order"],
    EventTypes = ["OrderSubmitted"],
    Actions = [new SubmitOrderAction()],
    Policies = [new LargeOrderPolicy()],
    StateMachines = [orderStateMachine],
};

6. Register at boot

import { registerFabricModules } from "@fabricorg/platform/modules";

registerFabricModules([orderModule]);
using FabricOrg.Platform.Registries;

var registry = new ModuleRegistry();
registry.Register(orderModule);

That's it. After this call:

  • order namespace is reserved
  • Order object type + OrderSubmitted event type are registered
  • The order.submit action, order.large_order_requires_approval.v1 policy, and Order state machine are all wired into the live platform registries

What happens when you invoke

@fabricorg/platform defines the contracts and the pipeline shape — but not a worker. You wire it into your runtime:

invokeAction
  → ActionInvocation row created (status: pending)
  → evaluatePolicies   (largeOrderPolicy runs; if total > 10k → block)
  → state-machine validation (verifies draft → submitted is allowed)
  → handler runs in transaction (your ctx.db work happens here)
  → AssetEvent appended (OrderSubmitted)
  → adapter steps (none on this action — skipped)
  → ActionInvocation status: completed

The runnable example in examples/minimal-vertical/src/index.ts ships a minimal in-process orchestrator that does exactly this — a great starting point for understanding the pipeline before plugging in your durable runtime (Temporal, queue worker, in-process executor, etc.).

On this page