---
title: Data model
description: The tables, how they relate, and the structural rule about where an amount is allowed to exist.
---

One PostgreSQL database, nine migrations. Every tenant-scoped table carries an
organization id, and every query in the data layer requires a tenant scope.

## By migration

### `0001` Tenancy and people

`organizations` · `workspaces` · `environments` · `users` · `members` ·
`roles` · `role_permissions` · `member_roles` · `sessions`

The hierarchy everything else hangs off. `environments` holds `gateway_mode`,
which is why promoting to `ENFORCE` is a tenancy operation.

### `0002` Agents

`agents` · `agent_identities` · `agent_passports` ·
`agent_capability_grants` · `capabilities`

`agent_identities` carries the public key and a **generation** counter. Rotation
bumps the generation, and an assertion signed with a superseded key is refused
with `IDENTITY_GENERATION_STALE` rather than a generic failure — so the agent's
logs say what actually happened.

### `0003` Tools and credentials

`tools` · `tool_agent_authorizations` · `resources` · `credentials` ·
`credential_leases`

`tool_agent_authorizations` is the join that makes registration grant nothing.
A tool existing is not a tool being callable.

`credentials` holds sealed material in a column the repository layer **does not
select into API responses**. `credential_leases` records what was issued, by
reference, never by value.

### `0004` Policy and delegation

`policies` · `policy_versions` · `policy_bindings` · `policy_test_cases` ·
`delegations` · `delegation_constraints`

`policy_versions` stores the rule set as JSON, and that JSON **is** the policy —
not a cache of something else. It is what the visual builder reads and writes.

`policy_bindings` carries the scope: organization, workspace, environment, agent
or tool.

### `0005` Risk, approval and trust

`risk_configurations` · `risk_events` · `risk_signals` · `approvals` ·
`approval_decisions` · `trust_evidence` · `trust_providers`

`risk_signals` stores each signal with its weight and its human-readable
explanation. Storing the explanation rather than regenerating it means a
six-month-old decision explains itself the same way it did at the time.

`approvals` is bound to the request that created it, which is what stops an
approval being replayed against a different payload.

### `0006` Actions and audit

`actions` · `action_stage_results` · `audit_events`

The two tables that grow without bound.

`action_stage_results` is one row per pipeline stage, which is what makes a
decision decomposable after the fact.

`audit_events` carries the hash chain: each row's hash covers its content and
the previous row's hash, keyed with `VEEFORE_AUDIT_CHAIN_SECRET`.

### `0007` Security and protocols

`incidents` · `emergency_actions` · `mcp_servers` · `mcp_tools` ·
`a2a_connections` · `registry_entries`

`emergency_actions` stores the blast radius **recomputed at execution time**, so
the evidence shows what was actually affected rather than what a preview
predicted some minutes earlier.

`mcp_tools` carries an `authorized` flag per tool, separate from discovery.

### `0008` Platform

`webhook_subscriptions` · `webhook_deliveries` · `platform_events` ·
`api_keys` · `usage_records` · `plans` · `subscriptions` ·
`sso_connections` · `scim_configurations` · `network_rules` · `jobs`

`api_keys` stores a hash. The plaintext is returned once at creation and never
again.

`jobs` is the queue. Claiming is tenant-safe and guarded against overlapping
poll ticks; abandoned jobs are reclaimed and dead-lettered after repeated
failure.

### `0009` Row-level security

No new tables. Applies PostgreSQL RLS policies to every tenant-scoped table when
`DATABASE_ENABLE_RLS=true`.

:::security Two independent isolation layers
The application layer requires a tenant scope on every query and refuses
without one. RLS exists so a bug in that layer does not become a cross-tenant
data leak.

Either alone would be sufficient. The point of defence in depth is that you do
not have to be confident the first layer is perfect.
[→ Tenant isolation](/secure/tenant-isolation)
:::

## The amount rule

This is enforced structurally by `npm run scope:audit`, not just documented.

**No table may have a balance column.** The audit asserts that no migration
declares `balance`, `available_balance`, `ledger_balance` or `funds`. A wallet
could be written without the word "wallet"; it could not be written without
somewhere to put a balance.

**An `amount` column may only exist on a policy or decision table.** The audit
parses every `CREATE TABLE`, finds those with an `amount` column, and fails if
any is not on this list:

| Table | Role of the amount |
| --- | --- |
| `actions` | What the agent asked to do. Decision context |
| `approvals` | What a human approved. Decision context |
| `delegations` | The ceiling granted. A policy input |
| `delegation_constraints` | A narrower ceiling. A policy input |
| `policy_versions` | A threshold in a rule. A policy input |
| `policy_test_cases` | A test input |
| `risk_events` | What was assessed. Decision context |
| `plans` · `subscriptions` · `usage_records` | Veefore's own SaaS pricing |

An amount column appearing anywhere else fails the build with the table named.
That is what makes the exclusion structural rather than aspirational.
[→ Scope](/scope)

## Identifiers

ULID-based with a type prefix, so ids are sortable by creation time and
self-describing. Full table in [Conventions](/reference/conventions).

An id is not a credential. Ids appear in URLs, logs and error messages; acting as
an agent requires its private key.

## Append-only in practice

`actions` and `audit_events` are only ever inserted. Nothing in the application
updates or deletes a row in either, outside the retention process — which
requires `audit:configure_retention`, is step-up gated, and writes a `CRITICAL`
audit event.

That is what makes the hash chain meaningful. A chain over a mutable table would
be a chain over something the application itself rewrites.

## Growth and retention

Both unbounded tables grow roughly in proportion to protected action volume.

Export before shortening retention. The export is watermarked and hashed, so the
archive stays verifiable. [→ Audit and export](/operate/audit-and-export)

## The schema catalogue

`packages/db/src/schema-catalog.ts` describes every table and column in one
place, used for validation and for the console's introspection. If you want the
column-level truth rather than this summary, read it — it is the source the
repositories are generated against.

## Related

:::cards

:::card Database | /operate/database
Migrations, pooling, RLS, backups.
:::

:::card Tenant isolation | /secure/tenant-isolation
Both layers in detail.
:::

:::card Scope | /scope
The exclusions and the audit behind them.
:::

:::card Repository layout | /architecture/repository-layout
Where everything lives.
:::

:::
