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:
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:
| Field | Type | Description |
|---|---|---|
event_id | string | Unique id for this envelope, assigned at creation. Deduplication on ingest keys off the caller's idempotency key; event_id identifies the stored event. |
reseller_id | string | null | The owning reseller. Null means Fibric-direct, no reseller in between. Present on every envelope. |
tenant_id | string | The owning tenant. Never null; an envelope cannot exist without a tenant. |
workspace_id | string | null | Optional workspace scoping inside the tenant, a team, a site, a project. Organizational, not a security boundary. |
source | string | Where 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_type | string | Dot-delimited type, for example order.created or hvac.zone.fault. Operator triggers match this field by glob. |
correlation_id | string | Ties an envelope to everything downstream of it, plans, actions, receipts, and to related envelopes. Generated if the caller does not supply one. |
payload | object | The event body, arbitrary JSON. Defaults to {}. |
agent_id | string | null | Set when the envelope was produced by an operator, identifying which one. |
session_id | string | null | Groups 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:
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 field | Type | Description |
|---|---|---|
reasoning | string, optional | The operator's stated rationale. Recorded for audit; carries no authority, the trust gate never reads it. |
actions | PlannedAction[] | The proposed steps, executed in order per entity. |
| PlannedAction field | Type | Description |
|---|---|---|
connector | string | The capability role the action targets, resolved to an installed connector by the tenant's bindings. |
tool | string | The tool on that connector, for example hold or notify. |
args | object | Tool arguments, validated by the tool's input schema before execution. |
value | number, optional | The monetary or risk magnitude of the action, for example a refund amount. Compared against maxValue policies by the trust gate. |
entity_key | string | Single-flight key; side effects serialize per entity (see above). |
idempotency_key | string | Deduplication 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:
| Disposition | Origin | Meaning |
|---|---|---|
ALLOW | trust gate | Permitted and executed. Reads always dispose as ALLOW; they need no policy and no idempotency. |
ALERT | trust gate | Permitted contingent on human approval. |
BLOCK | trust gate | Refused. The result carries ok: false and error: "blocked by trust policy". |
DEDUP | executor | The idempotency_key was already consumed; the side effect did not run again. ok: true, because the intended state already holds. |
| ActionResult field | Type | Description |
|---|---|---|
action | PlannedAction | The action as proposed, verbatim. |
decision | ActionDisposition | How the executor disposed it. |
ok | boolean | Whether the outcome is the intended state. |
result | unknown, optional | The connector's return value, when the action ran. |
error | string, optional | Why 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:
| Object | Key columns | Notes |
|---|---|---|
resellers | id, slug, name, branding | A partner running branded tenants. branding is jsonb, so white-labeling is data, not a fork. |
tenants | id, reseller_id, slug, name, branding | One customer organization. reseller_id null means Fibric-direct. slug is unique per reseller. |
workspaces | id, reseller_id, tenant_id, name | A 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:
| Prefix | Object | Example |
|---|---|---|
ev_ | Event (stored envelope) | ev_3a91c7 |
pl_ | Plan | pl_7c1a |
rc_ | Receipt | rc_5b21 |
op_ | Operator | op_8f2a1c |
cn_ | Connector installation | cn_7d2f4a |
exp_ | Receipt export job | exp_2f81 |
cur_ | Pagination cursor | cur_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.
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()
);
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.