Skip to content

Database Schema

Alokai CMS uses Cloudflare D1 (SQLite) as its primary database. Migrations live in apps/cms/migrations/.

Tables

organizations

Top-level multi-tenancy container.

CREATE TABLE organizations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

spaces

Content silo within an organization.

CREATE TABLE spaces (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id),
name TEXT NOT NULL,
slug TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

editions

An isolated container of an entire site within a space (formerly environments). See Editions & Markets for the full isolation model.

CREATE TABLE editions (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id),
name TEXT NOT NULL,
description TEXT,
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

markets

A storefront variant scoped to one edition (formerly stores). Each edition has exactly one default market; non-default markets inherit it and override per page. Slug uniqueness is per (space_id, edition_id).

CREATE TABLE markets (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id),
edition_id TEXT NOT NULL REFERENCES editions(id),
name TEXT NOT NULL,
slug TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (space_id, edition_id, slug)
);

users

CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
password_hash TEXT,
role TEXT NOT NULL DEFAULT 'viewer',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

Global roles: super_admin, admin, editor, viewer.

org_members / space_members

CREATE TABLE org_members (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id),
user_id TEXT NOT NULL REFERENCES users(id),
role TEXT NOT NULL, -- owner | admin | developer | member
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE space_members (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id),
user_id TEXT NOT NULL REFERENCES users(id),
role TEXT NOT NULL, -- admin | editor | author
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

content_models

CREATE TABLE content_models (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
edition_id TEXT NOT NULL,
name TEXT NOT NULL,
api_id TEXT NOT NULL,
description TEXT,
fields TEXT NOT NULL DEFAULT '[]', -- JSON array of field definitions
is_builtin INTEGER NOT NULL DEFAULT 0,
duplicated_from TEXT,
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

pages

CREATE TABLE pages (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
edition_id TEXT NOT NULL,
market_id TEXT, -- NULL = the edition's default market
content_type TEXT NOT NULL, -- page | categoryPage | productPage | layout
path TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
published_version INTEGER,
scheduled_at TEXT,
layout_id TEXT REFERENCES pages(id),
is_default_layout INTEGER NOT NULL DEFAULT 0,
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

page_versions

Immutable version snapshots.

CREATE TABLE page_versions (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
page_id TEXT NOT NULL REFERENCES pages(id),
version INTEGER NOT NULL,
locale TEXT NOT NULL DEFAULT 'en-US',
data TEXT NOT NULL DEFAULT '{}', -- JSON page content
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

components / component_versions

Same structure as pages/page_versions but for reusable components.

assets

Assets are space-scoped — shared across every edition in the space (no edition_id). The R2 object key is {orgId}/{spaceId}/{assetId}/{filename}.

CREATE TABLE assets (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size INTEGER NOT NULL,
r2_key TEXT NOT NULL,
alt_text TEXT,
width INTEGER,
height INTEGER,
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

webhooks

CREATE TABLE webhooks (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
edition_id TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
events TEXT NOT NULL DEFAULT '[]', -- JSON string array
headers TEXT NOT NULL DEFAULT '{}', -- JSON object
is_active INTEGER NOT NULL DEFAULT 1,
secret TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

api_keys

CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
prefix TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
permissions TEXT NOT NULL DEFAULT '[]', -- JSON string array
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_used_at TEXT
);

locales

CREATE TABLE locales (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
edition_id TEXT NOT NULL,
code TEXT NOT NULL, -- e.g. en-US
name TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0
);

preview_urls

CREATE TABLE preview_urls (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
edition_id TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

audit_log

CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
user_id TEXT,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
details TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

Additional tables

The schema has grown well beyond the core tables above. The following also exist; see the migrations for exact columns rather than relying on this list:

TablePurpose
page_locale_published_versionsPer-locale published-version tracking (a locale can publish independently).
locale_settingsLocale-detection configuration (path segment, query param, cookie, domain, TLD).
component_source / component_templatesRuntime-compiled remote components (Tier-1 sources, Tier-2 primitives).
translation_jobsOne row per AI translation request; tracks status and target locale. See Translations.
glossaryPer-space do-not-translate terms and per-locale overrides.
scheduled_actionsScheduled publish/unpublish for a page at a future time. See Scheduling & Approvals.
approval_requestsPublish-approval workflow rows, optionally linked to a scheduled action.
experiencesPer-page A/B test definitions (variants, impressions). See A/B Testing.
audiencesRule-based visitor segments with priority.
ab_visitor_aliasesSticky variant assignment across sessions/login.
design_system_settingsPer-edition design tokens (color, typography, spacing).

Migrations

Migrations live in apps/cms/migrations/ and are numbered sequentially (001_initial.sql … through the 046048 rename + edition-scoping set). The list is not exhaustive here — read the directory for the current state. Notable recent ones:

FileDescription
043_translation_jobs_env_scope.sqlScope translation jobs to the edition.
044_assets_space_scoped.sqlMake assets space-scoped (drop edition from the key).
046_rename_environments_to_editions.sqlRename environmentseditions, environment_idedition_id.
047_rename_stores_to_markets.sqlRename storesmarkets, store_idmarket_id.
048_markets_edition_scoped.sqlReplicate markets per edition; add edition_id; per-(space, edition) slug uniqueness.

Migrations also run idempotently on worker cold-start, so a fresh deploy self-heals its schema.

Run locally: yarn db:migrate Run on remote: wrangler d1 migrations apply alokai-cms-db --remote