Developer Resources
Data Model
The exact reference — every entity, enum, and constraint in the Kaiten schema, with the full ERD.
This page is the single exact reference for Kaiten's data model: the entities, their columns, the enums, and the constraints that hold the model together. Concept pages carry the mental model; when they say "exact reference", they mean here. It reflects the schema as of 2026-10-12 — 30 tables and 10 enums.
Examples use the Kaiten Sushi Shop dataset — the organization the seeder's demo profile loads on the Community Edition and the demo sandbox seeds on Kaiten Cloud: the Customers Sakura Tokyo, Ninja Osaka, Demo Restaurant, and Kappa Kyoto, six instances on versions of the Starter, Standard, and Premium Licenses, and entitlements like monthly-orders and menu-items. Seed data describes the dataset.
The ERD
Every domain table carries an organization_id — the organization is the isolation boundary, and deleting one cascades its entire subtree — with two exceptions. connector is the global registry of connector builds; what an organization activated is its own row in organization_connector. The three tables behind in-app notifications — notification_read, notification_user_state, and notification_preference — belong to a user and carry no organization.
Enums
| Enum | Values |
|---|---|
entitlement_type | BOOLEAN · NUMBER · CONFIG · NUMBER_AI_CREDIT |
aggregation_method | SUM · COUNT · AVERAGE · MAX · MIN · LATEST |
entitlement_reset_period | HOUR · DAY · WEEK · MONTH · YEAR |
entitlement_reset_anchor | CALENDAR · LICENSE_START |
license_type | DEVELOPMENT · TRIAL · PAID · COMMUNITY |
license_lifecycle_state | DRAFT · PUBLISHED · ARCHIVED |
instance_status | HEALTHY · DEGRADED · INCIDENT · MAINTENANCE |
metadata_field_resource_type | DEPLOYMENT_ZONE · INSTANCE |
token_kind | organization · platform |
user_type | human · machine |
There is no enforcement enum: how strictly a NUMBER grant is enforced is derived from two numbers on the license_entitlement row.
The hierarchy
organization
The root. You, the vendor.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
external_id | text | NOT NULL, unique — the identity provider's organization ID |
name | text | NOT NULL |
customer
Who you sell to — sakura-tokyo, ninja-osaka, demo-restaurant, kappa-kyoto.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK; (id, organization_id) unique |
name | text | NOT NULL |
slug | text | NOT NULL, unique per organization |
external_customer_id | text | nullable — your BYO-ID for this customer (CRM record, billing ID); indexed per organization |
domain | text | nullable |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
created_by_id / updated_by_id | uuid | NOT NULL, FK → user (delete: restrict) |
created_at / updated_at | timestamp | NOT NULL |
Deleting a customer cascades to its instances.
instance
One running copy of your product for one customer. The point of convergence: it references the customer, the license, and optionally a deployment zone.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
name / description | text | NOT NULL |
slug | text | NOT NULL, unique per organization |
customer_id | uuid | NOT NULL, composite FK (customer_id, organization_id) → customer (delete: cascade) |
license_id | uuid | NOT NULL, composite FK → license (delete: restrict) — one License version; see the trigger below |
deployment_zone_id | uuid | nullable, composite FK → deployment_zone (delete: set null) |
start_license_date | timestamp | NOT NULL, default now — the anchor of LICENSE_START usage windows |
end_license_date | timestamp | NOT NULL, default now + 1 year |
status | instance_status | NOT NULL, default HEALTHY |
lifecycle_stage | varchar | nullable |
metadata | jsonb | NOT NULL, default {} — validated against the organization's metadata fields |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
created_by_id / updated_by_id / timestamps | as on customer |
The FKs to customer, license, and zone are composite with organization_id — referencing another organization's record behaves like a record that doesn't exist. A license attached to any instance cannot be deleted. metadata is the only free-form column: every other property of an instance is a typed column.
A trigger function, instance_license_not_archived, runs when license_id is set — on insert, and on an update that changes it — and refuses a license whose lifecycle_state is ARCHIVED. It looks at nothing else: an instance already pinned to a version that is archived afterwards keeps its row and stays editable, and a DRAFT version is accepted.
The contract
license_family
The product a license is a version of — starter, standard, premium. A family has a slug and no name: the display name belongs to each version.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK; (id, organization_id) unique |
slug | text | NOT NULL, unique per organization |
last_version | int | NOT NULL, default 0 — the version counter: the highest number any version of the family has taken, deleted versions included |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
created_at / updated_at | timestamp | NOT NULL — updated_at moves when a version is added |
No operation writes a family directly: the first license of a product creates the row, each version added advances last_version, and deleting the family's last license deletes the row, which frees its slug.
license
What a customer bought, one version per row — starter; standard and standard-v2; premium, premium-v2, and premium-v3.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK; (id, organization_id) unique |
family_id | uuid | NOT NULL, composite FK (family_id, organization_id) → license_family (delete: cascade) |
name | text | NOT NULL — a display label, with no uniqueness: two families may share a name |
slug | text | NOT NULL, unique per organization |
description | text | NOT NULL |
type | license_type | NOT NULL |
version | int | NOT NULL; (family_id, version) unique — assigned on insert from the family's counter |
version_name | text | nullable — set to Version - <version> on insert when none is given |
lifecycle_state | license_lifecycle_state | NOT NULL, no column default — the API writes PUBLISHED unless a create asks for DRAFT |
is_default | bool | NOT NULL, default false — bound by the constraints below |
features | jsonb | nullable |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
created_at / updated_at | timestamp | NOT NULL |
Three rules hold the versions of a family together:
| Constraint | Rule |
|---|---|
license_family_id_version_key | (family_id, version) is unique. The update_license_version trigger numbers each row on insert as the family's last_version + 1 and advances the counter in the same statement, so a number is never reused — a deleted version leaves a gap |
license_family_id_is_default_key | a partial unique index on family_id where is_default — at most one default version per family |
license_default_must_be_published_check | is_default only on a row whose lifecycle_state is PUBLISHED — which is why archiving a family's default is refused |
lifecycle_state changes through the publish, archive, and unarchive operations only; License Types covers the transitions.
entitlement
A typed right in the catalog — monthly-orders (NUMBER), delivery-tracking (BOOLEAN), support-tier (CONFIG). The catalog carries the measurement: how a quota is counted, when it warns, when it resets. It carries nothing about enforcement.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
name | text | NOT NULL |
slug | text | NOT NULL, unique per organization |
description | text | nullable |
type | entitlement_type | NOT NULL |
aggregation_method | aggregation_method | required for NUMBER / NUMBER_AI_CREDIT, must be NULL for BOOLEAN / CONFIG (entitlement_number_aggregation_check) |
warning_threshold_percent | smallint | NOT NULL, 0–100, default 0 — 0 disables the warning event |
reset_period | entitlement_reset_period | nullable — NULL means a lifetime counter |
reset_anchor | entitlement_reset_anchor | nullable — set exactly when reset_period is set (entitlement_reset_anchor_required_check) |
unit_singular / unit_plural | text | NUMBER family only; both set or both NULL |
sale_unit_singular / sale_unit_plural / sale_unit_factor | text / float8 | all three set or none; require the base units; sale_unit_factor > 0 |
user_facing | bool | NOT NULL, default false |
display_order | int | NOT NULL, ≥ 0, default 0 |
icon | varchar | nullable |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
created_at / updated_at | timestamp | NOT NULL |
Two more CHECKs bound the periodic windows: reset_period is allowed only on the NUMBER family (entitlement_reset_period_number_family_check) and never with LATEST aggregation (entitlement_reset_period_latest_check). An entitlement a license still grants cannot be deleted (the grant's FK is restrict); the usage recorded for it is deleted with it (its FK cascades).
license_entitlement
The contract side: the value a license grants for an entitlement, and how strictly that value is enforced — premium-v2 grants delivery-tracking = true and 1,000 monthly-orders with a 20 % allowance; starter grants false and 20 as a hard cap.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
license_id | uuid | NOT NULL, FK → license (delete: restrict); (license_id, entitlement_id) unique — one grant per pair |
entitlement_id | uuid | NOT NULL, FK → entitlement (delete: restrict) |
value | jsonb | NOT NULL — an object { "type", "value" } where type is number, boolean, or object and value matches it (license_entitlement_value_check) |
limit_cap_exceeded_overage_percent | smallint | nullable — the enforcement of a numeric grant; see the two CHECKs below |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
created_by_id / updated_by_id / timestamps | as on customer |
The two CHECKs make enforcement fully derivable from the row, with no separate flag:
| Constraint | Rule |
|---|---|
license_entitlement_overage_percent_number_only_check | limit_cap_exceeded_overage_percent is NOT NULL exactly when value.type = 'number', and NULL otherwise |
license_entitlement_overage_percent_unlimited_check | when value.value = -1 (the unlimited sentinel) the percent is -1; when value.value ≠ -1 the percent is ≥ 0 |
Read together: -1 is unlimited (never refused), 0 is a hard cap (refused above the value), and a positive percent is a soft limit refused above value + value × percent / 100. The API names the column limitCapExceededOveragePercent and defaults it to -1 for an unlimited value and 0 otherwise when a grant omits it.
entitlement_usage
The observed side: current consumption of one entitlement on one instance.
| Column | Type | Constraints |
|---|---|---|
entitlement_id + instance_id | uuid | composite PK; FK → entitlement (cascade), FK → instance (cascade) |
value | jsonb | NOT NULL — { "type": "number", "value": <number>, "event_count": <integer ≥ 0> } (entitlement_usage_value_check) |
period_start | timestamp | nullable — the start of the window the total belongs to; NULL for a lifetime counter |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
There is no license_id: the license is reached through the instance. There is no history table either — the row holds the current window only, and closed windows exist as INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER events in the audit trail.
entitlement_group / entitlement_group_membership
A named set of entitlements with an aggregated usage view.
| Column | Type | Constraints |
|---|---|---|
entitlement_group.id | uuid | PK; slug unique per organization; name NOT NULL, description nullable |
entitlement_group_membership | composite PK (entitlement_group_id, entitlement_id); both FKs cascade; indexed by entitlement_id |
Feature delivery
feature_flags
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
slug | varchar | NOT NULL, unique per organization — delivery-tracking-enabled |
name | varchar | NOT NULL |
description | text | nullable |
type | varchar | NOT NULL |
enabled | bool | NOT NULL |
variants | jsonb | NOT NULL — the values the flag can resolve to |
default_variant | jsonb | NOT NULL — used when no targeting rule matches |
targeting_rules | jsonb | nullable — ordered CEL rules; first match wins |
event_name | varchar | NOT NULL |
metadata | jsonb | nullable |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
Releases and deployments
component
A versioned building block — unique per organization on (name, version).
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
name / version | varchar | NOT NULL; (organization_id, name, version) unique |
slug | text | NOT NULL, unique per organization |
description | text | nullable |
previous_component_id | uuid | nullable — links the prior version |
organization_id / created_by_id / created_at | NOT NULL |
release
An immutable bundle of components — unique per organization on version. There is no updated_at: a release is never edited.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
version | varchar | NOT NULL, unique per organization |
slug | text | NOT NULL, unique per organization |
description | text | nullable |
previous_release_id | uuid | nullable, FK → release (delete: restrict) |
organization_id / created_by_id / created_at | NOT NULL |
component_release joins the two: composite PK (component_id, release_id), both FKs restrict. A component a release bundles cannot be deleted. A release is held by two other FKs, both restrict: a deployment row that names it, and another release's previous_release_id — so a release the journal names, or that has a successor, cannot be deleted; deleting one that has neither removes its own component_release rows first.
deployment_zone
A logical execution environment, shared or dedicated.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK; (id, organization_id) unique |
name | varchar | NOT NULL |
slug | text | NOT NULL, unique per organization |
type | varchar | NOT NULL — the environment class; production on all three seeded zones |
description | text | NOT NULL |
metadata | jsonb | nullable — validated against the organization's metadata fields; the seeded zones carry region, compliance_profile, and dedicated |
organization_id / attribution / timestamps | as on customer |
deployment
The append-only journal: one row records one release landing in one zone. There is no update — history is the data.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
seq | bigint | NOT NULL, generated identity — the recording order of the journal |
deployment_zone_id | uuid | NOT NULL, FK → deployment_zone (delete: cascade) |
release_id | uuid | NOT NULL, FK → release (delete: restrict) |
created_at / created_by_id | NOT NULL — when and by whom | |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
Indexed by (organization_id, deployment_zone_id, created_at, seq) and (organization_id, release_id, created_at, seq): the journal reads per zone or per release, ordered by seq — two entries recorded in the same instant still have a definite order.
Metadata
metadata_field
An admin-declared, JSON-Schema-validated property for instances or deployment zones.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
resource_type | metadata_field_resource_type | NOT NULL |
key | text | NOT NULL; (organization_id, resource_type, key) unique among fields that are not archived (uq_metadata_field_key_active) |
label | text | NOT NULL |
json_schema | jsonb | NOT NULL — JSON Schema 2020-12 |
display_order | int | NOT NULL, default 0 |
archived_at | timestamp | nullable — archived fields stop validating writes, and release their key |
organization_id / attribution / timestamps | as on customer |
The trail and the event transport
audit_trail
Every domain event, recorded — see the audit trail for the concept.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
instance_id | uuid | nullable, FK → instance (delete: set null — entries outlive the instance) |
event_name / event_type | text | NOT NULL — the catalog name (ENTITLEMENT_USAGE_REPORT_REJECTED) and the dotted type |
occurred_at | timestamptz | NOT NULL, default now; indexed |
payload | jsonb | nullable — the event's data |
Indexed by (organization_id, instance_id) — the trail reads per instance — and by (organization_id, event_name, occurred_at, id) (idx_audit_trail_feed): one organization's entries for a set of event names, newest first, which is how the notification feed reads it.
notification_read
There is no notification table: a notification is an audit_trail row seen by a user. What is stored per user is a read state and a set of preferences, in three tables that reference the user and carry no organization_id.
notification_read marks one entry read by one user. It is sparse: a row exists only for an entry read individually.
| Column | Type | Constraints |
|---|---|---|
user_id | uuid | NOT NULL, FK → user (delete: cascade) |
audit_trail_id | uuid | NOT NULL, FK → audit_trail (delete: cascade) |
read_at | timestamptz | NOT NULL, default now |
Primary key (user_id, audit_trail_id).
notification_user_state
One row per user: the watermark that "mark all as read" moves. An entry is read when it has a notification_read row or occurred at or before read_all_before; moving the watermark deletes the individual marks it covers.
| Column | Type | Constraints |
|---|---|---|
user_id | uuid | PK, FK → user (delete: cascade) |
read_all_before | timestamptz | NOT NULL, default -infinity |
notification_preference
A user's choice for one event on one channel. It holds overrides only: no row means the event's default, so a new notifiable event, or a changed default, reaches every user who never chose otherwise.
| Column | Type | Constraints |
|---|---|---|
user_id | uuid | NOT NULL, FK → user (delete: cascade) |
event_name | text | NOT NULL — a notifiable event's catalog name |
channel | text | NOT NULL — in_app, the one channel served |
enabled | bool | NOT NULL |
Primary key (user_id, event_name, channel). Which events are notifiable, and their defaults, are not in the database: Notifications lists them.
outbox_events
The transactional outbox: a domain write and its events commit in the same transaction, and the change-data-capture pipeline reads them from here.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
event_name / event_type | text | NOT NULL |
occurred_at | timestamptz | NOT NULL, default now |
data | jsonb | NOT NULL — the event payload |
headers | jsonb | nullable |
inbox_events
The consumer side: each in-process consumer of the event stream records what it has already processed, so a redelivery is skipped without skipping another consumer's work.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
organization_id | uuid | NOT NULL, FK → organization (delete: cascade) |
source | text | NOT NULL — which stream delivered the message |
message_id | text | NOT NULL |
consumer | text | NOT NULL — which consumer processed it |
processed_at | timestamptz | NOT NULL, default now |
(organization_id, source, message_id, consumer) is unique: the dedup key is per consumer, which is what lets several consumers — the audit trail, a connector — share one delivery. Both transport tables are retention-swept on (occurred_at, id) / (processed_at, id) and are not part of the queryable model.
Identity
user
One table for humans and machines. Service accounts are machine users — the seeded ops-team and sdk among them.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
external_id | text | NOT NULL, unique — the identity provider's subject for humans |
type | user_type | NOT NULL, default human |
name | text | NOT NULL |
email | text | nullable, unique — machines must have none |
slug | text | nullable — machines must have one. Unique per organization among machine users (idx_unique_machine_slug_per_org), and among machine users without an organization (idx_unique_orgless_machine_slug) |
organization_id | uuid | nullable, FK → organization (delete: cascade) — machines must have one; the platform's own system:kaiten identity is the single exception |
deleted_at | timestamp | nullable |
created_at / created_by_id | created_by_id nullable, FK → user (delete: set null) |
Human membership in organizations goes through user_on_organization (composite PK organization_id, user_id, plus a nullable deleted_at).
token
API credentials — the ksh_ tokens of your service accounts, and the ksm_ platform credentials of a self-hosted operator.
| Column | Type | Constraints |
|---|---|---|
id | uuid | PK |
name | text | NOT NULL; (service_account_id, name, organization_id) unique among non-revoked tokens |
slug | text | NOT NULL; (slug, organization_id) unique |
hash / lookup_hash | text | NOT NULL — a salted verification hash and a deterministic lookup hash; the plaintext is never stored |
scopes | text[] | NOT NULL — see scopes |
kind | token_kind | NOT NULL, default organization |
organization_id | uuid | nullable, FK → organization (delete: cascade) |
service_account_id | uuid | NOT NULL, FK → user (delete: cascade) |
issued_by_platform_token_id | uuid | nullable, FK → token (delete: set null) |
expires_at | timestamp | nullable |
revoked_by / revoked_date | nullable — revocation is recorded, not deleted | |
created_by / created_at | NOT NULL |
Two CHECKs separate the credential families: an organization token carries an organization_id, while a platform token carries none and belongs to the system:kaiten service account (token_platform_is_orgless_system); only an organization token can have been issued by a platform token (token_platform_has_no_parent). That lineage is what lets revoking a platform credential cascade to the organization tokens it minted. Authentication covers the three families.
Connectors and CRM integrations
connector
The global registry of connector builds — one row per connector, not per organization, and so without an organization_id.
| Column | Type | Constraints |
|---|---|---|
name | text | PK |
version | text | NOT NULL |
settings_schema | jsonb | NOT NULL — must be a JSON object |
entitlement_slug | text | nullable — the slug of the BOOLEAN entitlement an organization's License must grant before it may activate this connector; NULL means ungated |
created_at / updated_at | timestamp | NOT NULL |
organization_connector
Which organizations activated which connector — the fact the settings store cannot answer on its own.
| Column | Type | Constraints |
|---|---|---|
organization_id + connector_name | composite PK; FK → organization (delete: cascade), FK → connector (delete: cascade) | |
activated_at | timestamptz | NOT NULL, default now |
created_at / updated_at | timestamptz | NOT NULL |
The row answers "activated?"; the connector's per-organization settings live in the secret store, not here. An activation survives neither its organization nor the connector registration it depends on.
customer_integrations / instance_integrations
Per-adapter links between a Kaiten record and an external system (e.g. Attio).
| Column | Type | Constraints |
|---|---|---|
customer_id (or instance_id) + adapter | composite PK | |
external_id | text | NOT NULL; (organization_id, adapter, external_id) unique |
metadata | jsonb | NOT NULL, default {} |
synced_at | timestamptz | NOT NULL |
last_error | text | nullable |
web_url | text | nullable — deep link into the external tool |
The schema also carries migration bookkeeping not listed here. What is listed is the model the API exposes and the docs reason about.
Where to go next
Architecture
How Kaiten is put together — the gateway, the API and its Platform listener, PostgreSQL, and the event pipeline.
Authentication
The three credential families — identity-provider JWTs, ksh_ access tokens, and ksm_ platform credentials — the identity model behind them, and the token lifecycle from issue to revocation.