Platform referenceEntities
Extending with vertical entities
How a vertical declares its object types, actions, events, and state machines through a FabricModule manifest.
A vertical adds entities to the platform by declaring a FabricModule and registering it at startup. There is no other extension point — no plugin discovery, no implicit registration, no auto-loading.
The module shape
From packages/platform/modules/index.ts:
interface FabricModule<TDb = unknown> {
namespace: string; // "orders"
version?: string;
displayName?: string;
description?: string;
dependencies?: ModuleDependencyInput[];
objectTypes?: ObjectTypeRegistrationInput[];
subjectTypes?: string[];
idPrefixes?: IdPrefixRegistration[];
eventTypes?: EventTypeRegistrationInput[];
actions?: ActionDefinition<TDb>[];
policies?: unknown[];
stateMachines?: unknown[];
displayRenderers?: ModuleDisplayRendererRegistration[];
adapterDefinitions?: unknown[];
permissions?: unknown[];
roles?: unknown[];
redactionRules?: unknown[];
dataClassifications?: unknown[];
retentionRules?: unknown[];
evidenceRenderers?: unknown[];
}public interface IFabricModule<TDb>
{
string Namespace { get; }
string? Version { get; }
string? DisplayName { get; }
string? Description { get; }
IReadOnlyList<IModuleDependencyInput>? Dependencies { get; }
IReadOnlyList<IObjectTypeRegistrationInput>? ObjectTypes { get; }
IReadOnlyList<string>? SubjectTypes { get; }
IReadOnlyList<IIdPrefixRegistration>? IdPrefixes { get; }
IReadOnlyList<IEventTypeRegistrationInput>? EventTypes { get; }
IReadOnlyList<IActionDefinition<TDb>>? Actions { get; }
IReadOnlyList<object>? Policies { get; }
IReadOnlyList<object>? StateMachines { get; }
IReadOnlyList<IModuleDisplayRendererRegistration>? DisplayRenderers { get; }
IReadOnlyList<object>? AdapterDefinitions { get; }
IReadOnlyList<object>? Permissions { get; }
IReadOnlyList<object>? Roles { get; }
IReadOnlyList<object>? RedactionRules { get; }
IReadOnlyList<object>? DataClassifications { get; }
IReadOnlyList<object>? RetentionRules { get; }
IReadOnlyList<object>? EvidenceRenderers { get; }
}A minimal example
// packages/orders/index.ts
import type { FabricModule } from "@fabricorg/platform/modules";
import type { Prisma } from "@example/database";
export const ordersModule: FabricModule<Prisma.TransactionClient> = {
namespace: "orders",
objectTypes: ["Order", "Customer", "Location", "Document"],
eventTypes: [
{ eventType: "PlanAccepted", schema: planAcceptedPayloadSchema, version: 1 },
{ eventType: "PlanDeclined", schema: offerDeclinedPayloadSchema, version: 1 },
],
actions: [acceptOfferAction, declineOfferAction],
policies: [creditPullConsentPolicy],
stateMachines: [offerStateMachine],
};// packages/orders/OrdersModule.cs
using FabricOrg.Platform.Modules;
public static class OrdersModule
{
public static IFabricModule<TDb> Create<TDb>()
{
return new FabricModule<TDb>
{
Namespace = "orders",
ObjectTypes = new[] { "Order", "Customer", "Location", "Document" },
EventTypes = new[]
{
new EventTypeRegistrationInput("PlanAccepted", planAcceptedPayloadSchema, 1),
new EventTypeRegistrationInput("PlanDeclined", offerDeclinedPayloadSchema, 1),
},
Actions = new[] { acceptOfferAction, declineOfferAction },
Policies = new[] { creditPullConsentPolicy },
StateMachines = new[] { offerStateMachine },
};
}
}Registration at startup
import { registerFabricModules } from "@fabricorg/platform/modules";
import { messagingModule } from "@example/messaging";
import { ordersModule } from "@example/orders";
registerFabricModules([messagingModule, ordersModule]);using FabricOrg.Platform.Modules;
using Example.Messaging;
using Example.Orders;
RegisterFabricModules(new[] { messagingModule, ordersModule });registerFabricModules does:
- Validates each manifest (namespace format, action ID format, dependencies).
- Topologically sorts by
dependencies(cyclic dependency → throws). - Registers each module's object types, subject types, event types, policies, state machines, actions, and display renderers — using the platform's
registerXIfMissinghelpers so re-registration is safe.
Validation rules
Enforced by validateManifest in packages/platform/modules/index.ts:
| Rule | Error |
|---|---|
namespace must be /^[a-z][a-z0-9-]*$/ | Invalid module namespace |
objectTypes PascalCase alphanumeric | Invalid object type |
actions[i].actionId matches /^[a-z][a-z0-9-]*\.[a-z][a-z0-9_]*$/ | Invalid action ID |
Action's namespace matches the module's namespace | declares namespace … but is registered by module … |
Action ID starts with <namespace>. | must start with module namespace |
mutatesDomain: true action with empty emitsEvents | mutates domain but emits no events |
| Display renderer for unknown event type | Display renderer registered for unknown event type |
| Module depends on a missing module | depends on missing module |
| Module depends on itself | cannot depend on itself |
| Dependency cycle | Cyclic Fabric module dependency detected |
| Two modules with the same namespace | Duplicate Fabric module namespace |
These checks run before any registration — a malformed module cannot partially register.
Module composition
The host app is the only thing that knows the full module list. The platform itself never imports a vertical.
What you cannot do
- Auto-register on import. Importing
@example/ordersdoes not register the module. The app must callregisterFabricModules. This is enforced in code review and by the boundary tests. - Register at request time. All registration happens at boot. Per-tenant module enabling (entitlements) is separate — the registry holds all modules, entitlements gate per-tenant use.
- Register a partial module. Validation runs before any side effects. Either the whole module registers or none of it does.
See also
- Module contract — how-to guide with a worked example.
- Vertical extensions pattern — design checklist for a new module.
- Built-ins — what is already pre-registered by the platform.