Fibric. Docs fibric.io →
v0.9 · preview
Platform

Data model

Everything in Fibric reduces to a small set of objects: an envelope records that something happened, a plan records what an operator proposes to do about it, an action is one governed step of that plan, and a receipt records how each step disposed. All of it is owned by exactly one tenant, optionally under a reseller. This page is the field-level reference for those objects, with names taken directly from the kernel types.

The object graph

The flow of one governed operation, left to right:

object flow
source system ──> EventEnvelope ──> operator ──> ExecutionPlan ──> executor ──> Receipt
                       │                              │                │
                  event_log row                 PlannedAction[]   ALLOW / ALERT /
                  (tenant-scoped)               each with          BLOCK / DEDUP
                                                entity_key +       per action
                                                idempotency_key

Every object in the chain carries the tenancy pair, reseller_id and tenant_id, and a correlation_id ties the chain together end to end: given a receipt you can walk back to the plan, the envelope, and the source event that started it. The architecture overview covers the flow; the sections below cover the fields.

Envelopes

The EventEnvelope is the one canonical event shape. A commerce webhook, a sensor reading, a cron tick, and an operator's own output all become envelopes, which is what makes the platform vertical-agnostic: a thermostat fault and a support message flow through identical machinery. The interface, from packages/kernel/src/envelope.ts:

FieldTypeDescription
event_idstringUnique id for this envelope, assigned at creation. Deduplication on ingest keys off the caller's idempotency key; event_id identifies the stored event.
reseller_idstring | nullThe owning reseller. Null means Fibric-direct, no reseller in between. Present on every envelope.
tenant_idstringThe owning tenant. Never null; an envelope cannot exist without a tenant.
workspace_idstring | nullOptional workspace scoping inside the tenant, a team, a site, a project. Organizational, not a security boundary.
sourcestringWhere the event came from: a connector ("shopify"), a hardware gateway ("bacnet-gw-7"), the scheduler ("cron"), or an operator's own output ("operator:jenny").
event_typestringDot-delimited type, for example order.created or hvac.zone.fault. Operator triggers match this field by glob.
correlation_idstringTies an envelope to everything downstream of it, plans, actions, receipts, and to related envelopes. Generated if the caller does not supply one.
payloadobjectThe event body, arbitrary JSON. Defaults to {}.
agent_idstring | nullSet when the envelope was produced by an operator, identifying which one.
session_idstring | nullGroups envelopes produced within one operator session.

Envelopes are constructed through makeEnvelope(input), which fills event_id, defaults reseller_id, workspace_id, agent_id, and session_id to null, defaults payload to {}, and generates a correlation_id when none is given. Over HTTP the same shape is created by POST /v1/events; see the Events API and the event envelope concept page.

Entities and entity keys

Fibric has no entity table. An entity is whatever real-world thing your actions must not trample concurrently, one order, one conversation, one HVAC zone, and it exists in the data model as a key: the entity_key that every planned action carries. Actions sharing an entity_key are serialized by the executor's single-flight gate; actions on different keys run independently.

Key discipline matters more than key format. Derive the key from the thing itself, stable across retries and across operators:

entity_key conventions
conversation:kustomer:64f2…      one support conversation
order:magento:SO-10884           one commerce order
asset:hvac:bldg-2:zone-14        one physical zone

The Events API also indexes events by the entity keys of the actions they triggered, so GET /v1/events?entity=order:magento:SO-10884 reconstructs everything that ever touched one order. See Single-flight & idempotency for the serialization semantics.

Plans and actions

An operator's entire output is an ExecutionPlan: optional reasoning plus an ordered list of actions. The operator proposes; the deterministic executor disposes. From packages/kernel/src/trust.ts:

ExecutionPlan fieldTypeDescription
reasoningstring, optionalThe operator's stated rationale. Recorded for audit; carries no authority, the trust gate never reads it.
actionsPlannedAction[]The proposed steps, executed in order per entity.
PlannedAction fieldTypeDescription
connectorstringThe capability role the action targets, resolved to an installed connector by the tenant's bindings.
toolstringThe tool on that connector, for example hold or notify.
argsobjectTool arguments, validated by the tool's input schema before execution.
valuenumber, optionalThe monetary or risk magnitude of the action, for example a refund amount. Compared against maxValue policies by the trust gate.
entity_keystringSingle-flight key; side effects serialize per entity (see above).
idempotency_keystringDeduplication key for the side effect. A replayed key disposes as DEDUP and does not run. This is the lock that made the 657-message flood structurally impossible.

Plans surface over HTTP with pl_ identifiers and a lifecycle (proposed, approval for ALERT actions, execution, veto); see the Actions & plans API.

Action results and dispositions

Each action the executor processes produces an ActionResult. Its decision field is an ActionDisposition, the three trust decisions plus one the executor adds itself:

DispositionOriginMeaning
ALLOWtrust gatePermitted and executed. Reads always dispose as ALLOW; they need no policy and no idempotency.
ALERTtrust gatePermitted contingent on human approval.
BLOCKtrust gateRefused. The result carries ok: false and error: "blocked by trust policy".
DEDUPexecutorThe idempotency_key was already consumed; the side effect did not run again. ok: true, because the intended state already holds.
ActionResult fieldTypeDescription
actionPlannedActionThe action as proposed, verbatim.
decisionActionDispositionHow the executor disposed it.
okbooleanWhether the outcome is the intended state.
resultunknown, optionalThe connector's return value, when the action ran.
errorstring, optionalWhy it did not run, or what failed when it did.

Receipts

A receipt is the durable, tenant-scoped record of one disposition: what was proposed, what the gate decided, what ran, what came back, and who approved or vetoed if a human was involved. Receipts are immutable and append-only; correcting course produces a new receipt (an undo is itself a receipted action), never an edit. They carry the correlation_id of their originating envelope, which is what makes the ledger walkable. Receipts have their own concept page, Receipts & audit, and their own API, including export.

Tenants, resellers, workspaces

Ownership is a three-level hierarchy defined in db/migrations/0001_tenancy.sql:

ObjectKey columnsNotes
resellersid, slug, name, brandingA partner running branded tenants. branding is jsonb, so white-labeling is data, not a fork.
tenantsid, reseller_id, slug, name, brandingOne customer organization. reseller_id null means Fibric-direct. slug is unique per reseller.
workspacesid, reseller_id, tenant_id, nameA working area inside a tenant. Tenant-scoped rows themselves, so the same RLS policy applies.

The tenant boundary is the hard wall, enforced by Postgres row-level security on every tenant table; the workspace boundary subdivides what is already inside it. The enforcement mechanics, the verbatim policy, the fibric_app role, are documented in Tenancy & isolation, and the environment implications in Environments.

Identifiers

API-surfaced objects use prefixed identifiers so an id is recognizable out of context:

PrefixObjectExample
ev_Event (stored envelope)ev_3a91c7
pl_Planpl_7c1a
rc_Receiptrc_5b21
op_Operatorop_8f2a1c
cn_Connector installationcn_7d2f4a
exp_Receipt export jobexp_2f81
cur_Pagination cursorcur_eyJpZCI6…

Internal rows, tenants, resellers, envelopes at rest, use UUIDs. entity_key and idempotency_key are caller-defined strings, not platform identifiers; the platform treats them as opaque.

Storage shape

The event log is the exemplar every tenant table follows: the envelope's fields become columns, the tenancy pair is physically present on the row, and tenant_id is NOT NULL with a foreign key, so a row without an owner cannot reach disk.

db/migrations/0001_tenancy.sql
CREATE TABLE IF NOT EXISTS event_log (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  reseller_id     uuid,
  tenant_id       uuid NOT NULL REFERENCES tenants(id),
  workspace_id    uuid,
  source          text NOT NULL,
  event_type      text NOT NULL,
  correlation_id  uuid NOT NULL,
  payload         jsonb NOT NULL DEFAULT '{}',
  created_at      timestamptz NOT NULL DEFAULT now()
);
i
Names match the source

The field names on this page, event_id, entity_key, idempotency_key, correlation_id, and the rest, are the names in the kernel source and the API. If a name here ever disagrees with what the API returns, the API is right and this page has a bug; report it.

Continue with the event envelope for envelope semantics in depth, the API overview for the HTTP view of these objects, and Reliability and delivery semantics for how they behave under retries and failures.