LumiBaseDocs

Data Model (Drizzle / Postgres)

This document describes the tables declared in packages/database/src/schema/. Internal database rows use text IDs by default. No-code content collections also carry a logical item primary key strategy through collections.primaryKeyField and collections.primaryKeyType.

Every tenant-scoped domain table has site_id.

Physical table names carry a lumibase_ prefix. Every LumiBase system table is physically named lumibase_<name> (e.g. the logical users table is physically lumibase_users), created by the 0000_lumibase_init migration. This reserves the lumibase_ namespace for the platform, so any table WITHOUT that prefix is unambiguously user-created (or a mat_* materialization). See ADR-010. The section headings below use the short logical name for readability; the summary table lists the physical names.

Schema files are split by domain:

FileBảng (physical)
core.tslumibase_sites, lumibase_users, lumibase_user_sites, lumibase_teams, lumibase_team_members, lumibase_notifications
access.tslumibase_roles, lumibase_policies, lumibase_role_policies, lumibase_user_policies, lumibase_permissions, lumibase_refresh_tokens
cms.tslumibase_pages, lumibase_collections, lumibase_fields, lumibase_relations, lumibase_items, lumibase_revisions, lumibase_releases, lumibase_release_items, lumibase_activity, lumibase_flows, lumibase_flow_runs, lumibase_operations, lumibase_materialized_collections
platform.tslumibase_folders, lumibase_files, lumibase_presets, lumibase_translations, lumibase_settings, lumibase_webhooks, lumibase_extensions, lumibase_translation_memory, lumibase_glossary, lumibase_push_subscriptions
ai.tslumibase_ai_approvals, lumibase_agent_*
firebase-sync.tslumibase_firebase_sync_pipelines, lumibase_firebase_sync_log
external-auth.tslumibase_auth_external_issuers

Migrations live in packages/database/migrations/ and packages/database/drizzle/.


1. Core tenancy & identity (core.ts)

sites

  • id, name, domain, createdAt, plus identity/branding/theme columns and defaultLanguage, defaultAppearance, and defaultSaveAction (stay|return|create_new, default stay — the site-wide default Studio save action; per-user override lives in users.preferences.saveAction).

users

ColumnTypeNote
idtext PKnanoid
logtoIdtext uniqueOIDC subject
email, firstName, lastName, avatartext
statustextactive/invited/suspended
language, theme, tfajsonbpreferences
preferencesjsonbper-user UI prefs: { language, theme, timezone, defaultPresets, saveAction }. saveAction (stay/return/create_new) overrides sites.default_save_action.
lastSeenAttimestamp
passwordChangedAttimestampSet on every reset/change (migration 0006). A password-reset token whose iat predates it is rejected → single-use reset links.
createdAt, updatedAttimestamp

Unique indexes: users_external_id_unique, users_is_bootstrap_unique (partial, is_bootstrap = true), and users_email_lower_unique on lower(email) (migration 0006) — email is identity-global, one account per address. ⚠️ Migrating an existing instance fails if it already holds case-insensitive duplicate emails; de-duplicate first.

user_sites (membership N-N)

  • userId, siteId, roleId, joinedAt. PK composite.

teams / team_members

  • teams: id, siteId, name, description.
  • team_members: teamId, userId. PK composite.

notifications

  • Persistent inbox for mentions, denial reasons, and system events. Realtime delivery is handled above this persistence layer.

2. Schema (no-code) — cms.ts

pages

  • Page-builder pages consumed by /deliver.

collections

ColumnTypeNote
idtext PK
siteIdtext FK
nametextmachine name, unique per site
label, pluralLabeltextauthor-facing labels
hidden, systemboolean
singletonboolean
icon, color, notetext
primaryKeyFieldtextlogical item identifier field, defaults to id
primaryKeyTypetextnanoid, uuid, integer, bigInteger, string
storageModetextjsonb, materialized, physical, external
displayTemplatetextmustache template default
sortField, archiveField, archiveValue, unarchiveValuetext
itemDuplicationFields, translationsjsonb
accountabilitytextall / activity / none
versioningboolean
metajsonbextra UI hints, including safe system-field presentation overrides
createdAt, updatedAt

Primary key strategy:

  • nanoid: default logical string identifier generated by LumiBase.
  • uuid: service-generated UUID string.
  • string: caller-provided string identifier.
  • integer / bigInteger: sequence-backed strategies reserved for materialized/physical storage support; JSONB collections block these combinations.

Storage modes are intentionally explicit:

  • jsonb: default logical collection backed by items.data; fastest schema evolution, no runtime DDL, advisory SQL-native indexes/unique constraints.
  • materialized: JSONB source of truth plus managed physical read projection for hot paths.
  • physical: future Directus-like owned table mode; schema diffs mark this as a storage runtime change.
  • external: future introspected table mode; destructive DDL/relation actions are limited because LumiBase does not own the table.

fields

ColumnTypeNote
idtext PK
siteId, collectionIdtext FK
nametextmachine
typetextstring,text,integer,decimal,boolean,json,uuid,date,datetime,time,csv,hash,geometry,alias
interfacetextUI editor key
displaytextdisplay formatter key
options, displayOptions, validation, conditions, translationsjsonb
required, readonly, hidden, encrypted, versioned, rawEnabledboolean
classificationtextData sensitivity (Req 5): none/internal/pii/phi. pii/phi must be encrypted=true; drives default masking, read_decrypted gating, and field_access_log audit.
sortOrderinteger
width, grouptext

Compiled schemas expose generated system fields in addition to user-defined rows:

FieldTypeNotes
idstringPrimary item identifier, readonly/generated.
statusstringWorkflow status; visible when status/archive behavior is enabled.
sortintegerManual ordering value.
user_createdstringCreator user ID, readonly/generated.
user_updatedstringLast updater user ID, readonly/generated.
created_atdatetimeCreation timestamp, readonly/generated.
updated_atdatetimeLast update timestamp, readonly/generated.
deleted_atdatetimeSoft-delete timestamp, readonly/generated and hidden.

relations

  • id, siteId, manyCollection, manyField, oneCollection, oneField, junctionCollection?, type, aliasField?, relatedDisplayTemplate?, junctionManyField?, junctionOneField?, sortField?, onDelete, meta jsonb.
  • Relation types are m2o, o2m, m2m, with m2a reserved.
  • The schema service validates referenced collections and fields, and blocks collection deletes when relations still reference either side.

items

ColumnType
idtext PK
siteId, collectionIdtext FK
statustext
datajsonb
sortinteger
userCreated, userUpdatedtext FK users
publishAt, unpublishAttimestamp nullable
editorialStatetext nullable
dekWrappedtext nullable
createdAt, updatedAttimestamp
deletedAttimestamp nullable

Indexes: (siteId, collectionId, status), GIN on data, (siteId, status, publishAt), (siteId, status, unpublishAt).

revisions

  • id, siteId, itemId, collectionId, delta jsonb, parentId, userId, createdAt.

releases (Content Releases)

  • A cross-collection publish bundle. id, siteId, name, description, status (draft|scheduled|published|failed|partially_failed), atomicityMode (all_or_nothing|best_effort), publishAt, publishedAt, maintenanceWindow jsonb, statusReason, createdBy → users.id (set null), createdAt, updatedAt.
  • Indexes: releases_site_status_idx (siteId, status), releases_publish_due_idx (siteId, status, publishAt) (mirrors items.publishDueIdx for the scheduler sweep).

release_items (Content Releases)

  • Junction release ↔ item, optionally pinned to one revision. id, siteId, releaseId → releases.id (cascade), collection, itemId → items.id (cascade), targetStatus (default published), revisionId → revisions.id (set null), outcome (published|skipped|failed), outcomeReason, createdAt.
  • Unique (releaseId, collection, itemId) (upsert key); index (siteId, releaseId).

activity

  • id, siteId, action, userId, collection, itemId, ip, userAgent, comment, payload jsonb, createdAt.

flows (POST-GA3)

ColumnTypeNote
id, siteIdPK + FK
name, descriptiontext
statustextactive / inactive / draft
triggerTypetextwebhook / event / schedule / manual
triggerOptions, graphjsonbgraph: { entry?, nodes: [{ id, key, options, next?, onError? }] }
nextRunAttimestampdùng cho schedule trigger
accountabilitytext

flow_runs

  • Mỗi run lưu status, input, steps (per-node output), output, error, startedAt, finishedAt.

operations

  • Khai báo từng operation node trong flow (key + type + options + position).
  • Type: condition / transform / http / mail / log / sleep / run-extension / item.create|update|delete / notify.

materialized_collections (POST-GA6)

  • Defines denormalized read tables for hot paths.
  • target, refreshStrategy (auto/cron/manual), refreshCron, projection jsonb, filter jsonb, lastRefreshedAt, rowCount, status, error.

Schema diff/apply runtime contract

The schema service exposes a Directus-parity lifecycle:

  • POST /api/v1/collections/diff compares proposed collection metadata, fields, and relations with the current schema.
  • PUT /api/v1/collections/{name}/schema validates and applies metadata, field, and relation changes transactionally when the database runtime supports transactions.
  • Apply invalidates compiled schema, permission, and typegen cache keys and emits a schema.changed event.

Diff output includes root risk, runtimeImpact, and per collection/field/relation entries. Runtime impact values include cache_invalidation, permission_recompile, typegen_rebuild, data_migration_required, relation_reindex, and storage_runtime_change.

Typegen manifest v2

GET /api/v1/typegen/schema returns a versioned manifest with:

  • collection primaryKey, primaryKeyField, and primaryKeyType;
  • user fields plus compiled system fields;
  • required, nullable, readonly, generated, system, encrypted, and primaryKey flags;
  • relation descriptors for expanded response types.

SDK generation uses this manifest to emit base collection interfaces and CollectionExpanded relation response types.

3. Permissions (access.ts)

roles

  • id, siteId, name, description, icon, adminAccess boolean, appAccess boolean.
  • Note: adminAccess/appAccess are legacy compatibility flags. New RBAC work migrates these flags to policies so roles remain grouping units. See Role Flag to Policy Flag Migration.

policies

  • id, siteId, name, description, rules jsonb. Policy độc lập có thể attach vào nhiều roles/users.
  • Explicit flags: adminAccess, appAccess, enforceTfa, ipAllow, ipDeny, validFrom, validUntil. These are the new source of truth for admin/app/TFA/IP/time guards.

role_policies / user_policies

  • Many-to-many với priority. user_policies cho phép gán policy trực tiếp user (override role).

permissions

  • id, siteId, policyId, collection, action (create/read/update/delete/share), permissions jsonb (row-level rule DSL), validation jsonb, presets jsonb, fields text[] (field-level allow list, * = all).

refresh_tokens (migration 0005, physical lumibase_refresh_tokens)

  • id, siteId, userId, audience (studio/frontend), tokenHash (sha256, unique — plaintext never stored), familyId (rotation chain), replacedBy, expiresAt, revokedAt, lastIp, lastUserAgent, createdAt.
  • Rotating, revocable session-renewal tokens. Reuse of a revoked row revokes the whole familyId (theft detection). Swept on the hourly cron once expired. See security/user-management.md §4d.

System collections seed permissions

When seeding local/staging/prod, seed system collection permissions explicitly. Sensitive collections such as system_state, audit_log, login_attempts, login_baselines, admin_backup_codes, scim_tokens, and future API key tables are admin/security-only. See System Collections & Sensitive Access.

4. Files & Assets (platform.ts)

files

  • id, siteId, storage (r2/s3/external), filenameDisk, filenameDownload, mime, filesize, width, height, duration, folder, metadata jsonb, uploadedBy, createdAt.

folders

  • id, siteId, name, parent.

5. UX state (platform.ts)

presets (bookmark + view state)

  • id, siteId, bookmark text nullable, collection, userId?, roleId?, layout (tabular/cards/kanban/calendar/map), layoutQuery jsonb, layoutOptions jsonb, search, filter jsonb, icon, color, refreshInterval.

translations (UI strings + content)

  • id, siteId, language, namespace (ui/field/content), key, value. Unique (siteId, language, namespace, key).

6. Settings & Config (platform.ts)

settings (key/value per site)

  • id, siteId, key, value jsonb, scope (site/module), updatedAt.

webhooks

  • id, siteId, name, url, actions text[], collections text[], headers jsonb, status, secret, createdAt.

7. Extensions + Marketplace (platform.ts)

extensions

ColumnTypeNote
idtext PK
siteIdnullable text FKnull = global
name, versiontext
typetexthook/endpoint/module/interface/display/layout/panel/operation
enabledboolean
bundleUrltextR2/S3 path
manifestjsonb
capabilitiesjsonbgranted subset of manifest
installedBy, installedAt
Marketplace fields (POST-GA5): signature, signatureAlg (ed25519/rsa-pss-sha256), publisherKeyId, publisher, marketplaceSlug, publishedAt, bundleSha256

Indexes: (siteId, name), (publisher, publishedAt), marketplaceSlug.

8. Translation Memory (platform.ts, POST-GA1)

translation_memory

  • (sourceLang, targetLang, sourceText, targetText) + quality (0-100), source (human/mt/imported), provider, hits, context.

glossary

  • Term-level constraints: rule (do-not-translate/prefer/forbidden), term, translation, note.

9. Realtime / Notifications (core.ts)

notifications

  • id, siteId, recipient (userId), sender?, subject, message, collection?, item?, status, createdAt.

Realtime cursor data (CRDT-lite) không persist trong Postgres — chỉ broadcast qua Durable Object/host process. Xem apps/cms/src/services/cursor-protocol.ts.

10. AI Copilot and Agent Harness (ai.ts)

The existing Copilot tables provide chat/HITL history. The Agent Harness extends that foundation with first-class lifecycle, tools, approvals, artifacts, evaluations, and memory. See Agent Harness Layer for the user-facing and runtime contract.

ai_approvals

ColumnTypeNote
idtext PKnanoid(21)
siteIdtext FK → sites CASCADE
agentNametextdefault 'lumibase-copilot'
skillNametexttừ CORE_SKILLS registry
argumentsjsonbđối số skill
statustextpending / approved / rejected
contexttext nullablemessage gốc của user
createdAt, decidedAttimestamp
decidedBytext FK → users SET NULL

Index: (siteId, status).

Behavior: dangerous skills (schema:write or names starting with delete) must create an ai_approvals row and wait for approval instead of executing directly. See AI Copilot. New dangerous harness actions also write linked/generalized agent_approvals records where applicable.

agent_* harness tables

CollectionPurpose
agent_goalsBusiness goals created by a user, workflow, or generation template
agent_runsIndividual execution attempts with status, budget, metrics, and retry linkage
agent_plansPlanned steps associated with a run
agent_toolsTool registry entries with schemas, owner, enabled state, capability, risk, and rate policy
agent_permissionsAgent/role/policy capability grants
agent_tool_callsScoped audit records for each tool call or denial
agent_approvalsGeneralized approvals for plans, tool calls, artifacts, and schema diffs
agent_artifactsVersioned generated outputs such as schema diffs, specs, seed data, prompts, and migrations
agent_evaluationsEvaluation results used as gates before approval/publish
agent_memoryScoped long-lived memory with provenance, confidence, expiry, and optional embedding

All harness tables include siteId and must be queried through tenant-scoped filters.

Operational notes:

  • POST /api/v1/agent/generate-app creates page_spec, component_spec, seed_data, and api_spec artifacts and evaluates each artifact before returning.
  • agent_runs.metrics.stopReason records completion or budget stop reasons such as completed, error, and max_tool_calls.
  • agent_tool_calls.cost stores token/cost estimates after secret masking.
  • If the same goal fails at least three runs and a runtime QueueProvider is available, the run service enqueues agent-dead-letter with the scoped run failure payload.

Content OS tables (content-os.ts)

CollectionPurpose
content_intentsDeclared desired state (SLO) per collection: rules, cron schedule, budget, autonomy cap, maintenance window
content_driftsDetected rule violations; unique (siteId, fingerprint) dedupes detection across scans
agent_autonomy_grantsEarned trust per (site, agentRole, capability): level 0–4, evidence, expiry
agent_incidentsDemotion evidence and exception-inbox feed (veto, eval_fail, load_guard, …)
agent_freezesKill-switch freezes (scope site/role); liftedAt IS NULL = active; doubles as audit trail
agent_rolesMulti-agent role library with minimal capability sets; harness enforces role ∩ grant
constitutionsVersioned publish-gate evaluator sets; sha256 hash identity; at most one active per site

Content OS columns on existing tables:

  • revisions: authorType, createdByRunId, model, constitutionHash, sources, confidence, staged, autoCommitAt (provenance + veto staging).
  • items: pinnedFields (Law Zero — fields a human edit locked against agent writes).
  • agent_goals: parentGoalId (goal tree), origin (user/reconciler/planner/flow), intentId, driftFingerprint, agentRole.
  • agent_approvals: kind (approval/veto), autoCommitAt, approverType (human/agent), approverRunId.

Directus-inspired tables (cms.ts)

CollectionPurpose
content_versionsNamed parallel draft branches of an item, distinct from linear revisions. Snapshots item data + a hash of main at snapshot time (divergence detection). Promote applies a version to main via ItemService (writes a revision). Unique (siteId, collectionId, itemId, key). See .kiro/specs/content-versioning.
dashboardsInsights dashboard container per site (name/icon/color/note).
panelsOne visualization on a dashboard: type (metric/timeSeries/bar/pie/list/table), position ({x,y,w,h}), query (a PanelQuery), options. Aggregates run safely (field whitelist + siteId scope). See .kiro/specs/insights-dashboard.
transform_presetsNamed image-transform presets: URL-safe key → a TransformDsl ({ width?, height?, format?, quality?, fit?, focal? }). Resolved by GET /media/:key?preset=<key>. Unique (siteId, key). Migration 0004_transform_presets. See .kiro/specs/image-transform-dsl.

11. Firebase Sync (firebase-sync.ts)

Xem features/firebase-sync.md. Migration: 0000_lumibase_init (consolidated).

11d. Change Feed (cdc.ts — spec cdc-extension-integration)

TablePurpose
lumibase_cdc_change_eventsAppend-only transactional outbox: one row per committed mutation (resource item/collection/field/setting — default item, migration 0008; collection, item_id, operation, masked payload, changed_fields, actor/source, occurred_at). The envelope type is <plural-resource>.<operation>. Feed order = keyset (occurred_at, id) — nanoid PKs carry no order. Indexes (site_id, occurred_at, id) and (site_id, collection, occurred_at, id).
lumibase_cdc_subscriptionsConsumer registry + checkpoint (cursor_occurred_at + cursor_id), kind pull/webhook/extension, status active/paused/dead/stale, filters, consecutive_failures. Unique (site_id, name).
lumibase_cdc_deliveriesAppend-only delivery-attempt log per batch (attempt, status, http status, bounded error message, duration). Pruned on the same retention window as the outbox.

All three are site_id-scoped with site_isolation RLS. See docs/en/features/cdc-change-feed.md.

11c. External JWT auth (external-auth.ts)

auth_external_issuers

  • Per-site trusted external JWT issuer. Public config only — no secrets (signatures verify against the issuer's JWKS). id, siteId (FK sites, cascade), issuer (matches the iss claim), jwksUri/discoveryUrl (one required), audience (jsonb: string|string[]), algorithms (jsonb: asymmetric allowlist), claimMapping (jsonb: { email, roles, siteId?, externalId? }), roleMapping (jsonb: { "<claim role>": { roleId|systemKey } }), defaultRoleId, jitProvisioning, clockSkewSeconds, enabled, createdAt, updatedAt.
  • Unique (siteId, issuer); index (siteId, enabled). Migration: 0000_lumibase_init (consolidated). See security/external-jwt-auth.md.

lumibase_firebase_sync_pipelines

ColumnTypeNote
idtext PKnanoid
siteIdtextFK sites.id (cascade)
nametextunique theo (siteId, name)
targettextfirestore / rtdb
statustextactive / paused / error
statusMessagetextlỗi gần nhất (nullable)
projectIdtextFirebase project id
credentialsEncryptedtextcredential blob mã hoá AES-GCM (write-only)
collectionsjsonbmachine-names; [] = mọi collection
targetPathtexttemplate, mặc định {collection}
syncOnCreate / syncOnUpdate / syncOnDeleteinteger1/0 bật từng action
lastSyncAttimestamplần sync thành công gần nhất
lastSyncItemCountintegernullable
createdAt / updatedAttimestamp

lumibase_firebase_sync_log

ColumnTypeNote
idtext PKnanoid
pipelineIdtextFK lumibase_firebase_sync_pipelines.id (cascade)
siteIdtextFK sites.id (cascade)
collection / itemIdtextnguồn của thay đổi
actiontextcreate / update / delete
resulttextsuccess / error
errorMessagetextnullable
durationMsintegerround-trip gọi Firebase REST
recordedAttimestampindex (pipelineId, recordedAt)

11b. Regulated / sensitive content (regulated.ts)

Opt-in, additive tables for the regulated-content-readiness capability set. Default Tier 1 installations never write to them. ID convention: nanoid for all (domain + audit-grade), matching the existing audit_log PK.

encryption_keys

Metadata only for key versioning/rotation (Req 3.3) — never stores key material; the bytes live in the runtime KeyProvider (Workers Secrets / env).

ColumnTypeNote
idtext PKnanoid
siteIdtext FK nullablenull ⇒ global key
keyIdtextversion id embedded in the ciphertext envelope (e.g. v1)
statustextactive (encrypt new) / retired (decrypt only)
algotextdefault AES-GCM
createdAt, retiredAttimestamp
Unique: (siteId, keyId).

field_access_log

Audit of every decrypted read of a pii/phi field (Req 6). Never stores the decrypted value; written in batches, site-isolated by RLS.

ColumnTypeNote
idtext PKnanoid
siteIdtext FK
collectiontext
recordIdsjsonbaffected record ids (aggregate for list reads)
fieldsjsonbfield names decrypted
actor, action, requestIdtext
timestamptimestamp
Indexes: (siteId, timestamp), (actor, timestamp).

content_reviews

Human editorial sign-off records (Req 9). itemId is onDelete: set null so review history survives erasure (Req 11.3).

ColumnTypeNote
idtext PKnanoid
siteIdtext FK
itemIdtext FK nullableset null on item delete
revisionIdtext
requestedBy, decidedBytext FK users (set null)
assignedTotextuser id or role token
statustextpending/approved/rejected
reasontext
decidedAt, createdAttimestamp
Indexes: (siteId, status), (siteId, assignedTo).

erasure_requests

GDPR right-to-erasure lifecycle (Req 11). Stores a hash of the subject identifier, never plaintext; supports dual-control.

ColumnTypeNote
idtext PKnanoid
siteIdtext FK
scopejsonb{ collection, filter }
subjectHashtexthash of subject id (never plaintext)
reasontext
requestedBy, confirmedBytext FK users (set null)confirmedBy = second admin (dual-control, Req 11.4)
statustextpending/confirmed/executing/completed/failed
recordCountinteger
createdAt, completedAttimestamp
Index: (siteId, status).

11d. Git Integration (git-integration.ts)

Per-tenant GitHub/GitLab connections + cached PR/CI state, raw webhook log, ephemeral preview environments, and commit↔content provenance. All tables carry site_id (cascade) and are registered for RLS. Physical names carry the lumibase_ prefix (ADR-010). Migration 0009_git_integration.

Table (physical)PurposeKey columns
lumibase_git_integrationsOne repo connection per (site, provider, repo)provider, repo_full_name, auth_method (app|pat), installation_id, encrypted_token, webhook_secret_enc, status, scopes, sync_config
lumibase_git_pull_requestsCached PR/MR stateunique (integration_id, number); state, ci_status, mergeable, head_sha, preview_url, raw
lumibase_git_ci_runsCI run + jobs + stored-log refunique (integration_id, provider_run_id); status, jobs, duration_ms, log_ref (runtime blob)
lumibase_git_webhook_eventsRaw inbound events (replay-able)unique (provider, delivery_id); event, payload, processed, error
lumibase_git_preview_envsEphemeral preview site per PRunique (pr_id); ephemeral_site_id, status, url, expires_at
lumibase_git_provenanceCommit/PR → content/schema/intent linkcommit_sha, pr_number, collection, item_id, change_type

Tokens + webhook secrets are encrypted at rest via CryptoService (AES-GCM, AAD-bound to { siteId, integrationId, field }); plaintext is never stored.

12. Indexing & RLS

  • Bắt buộc index (siteId, …) ở mọi bảng domain.
  • Áp dụng Drizzle helper scopeSite(siteId) ở tầng repo.
  • Postgres RLS được bật qua middleware withRls() (apps/cms/src/middleware/rls.ts) — set session var để defence-in-depth.
Last modified: 23/07/2026