-- ═══════════════════════════════════════════════════════════════════════════ -- Originmain — Initial Schema -- Run: supabase db push (or paste into Supabase SQL Editor) -- ═══════════════════════════════════════════════════════════════════════════ -- ── Extensions ─────────────────────────────────────────────────────────────── create extension if not exists "uuid-ossp"; -- ── Enums (CHECK constraints on text columns) ───────────────────────────── -- Using text + CHECK rather than postgres enums so adding values is a -- non-destructive ALTER rather than a DROP/RE-CREATE cycle. -- ── Tables ─────────────────────────────────────────────────────────────────── -- workspaces create table if not exists workspaces ( id uuid primary key default uuid_generate_v4(), name text not null, owner_id text not null, -- Clerk user ID plan text not null default 'FREE' check (plan in ('FREE','TEAM','ENTERPRISE')), settings_jsonb jsonb not null default '{}', created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists workspaces_owner_id_idx on workspaces (owner_id); -- origins create table if not exists origins ( id uuid primary key default uuid_generate_v4(), type text not null check (type in ('GIT_COMMIT','LINEAR_ISSUE','SLACK_MESSAGE','URL','FORK')), source_ref text not null, source_metadata_jsonb jsonb not null default '{}', created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- artboards create table if not exists artboards ( id uuid primary key default uuid_generate_v4(), workspace_id uuid not null references workspaces (id) on delete cascade, name text not null, origin_id uuid references origins (id) on delete set null, parent_artboard_id uuid references artboards (id) on delete set null, -- metadata_jsonb stores canvas position/size and optional renderUrl: -- { x, y, width, height, renderUrl? } metadata_jsonb jsonb not null default '{}', created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists artboards_workspace_id_idx on artboards (workspace_id); create index if not exists artboards_parent_artboard_id_idx on artboards (parent_artboard_id); create index if not exists artboards_created_at_idx on artboards (created_at desc); -- intent_diffs create table if not exists intent_diffs ( id uuid primary key default uuid_generate_v4(), artboard_id uuid not null references artboards (id) on delete cascade, author_id text not null, -- Clerk user ID changes_jsonb jsonb not null default '{}', summary text not null default '', status text not null default 'DRAFT' check (status in ('DRAFT','EXPORTED','IMPLEMENTED','BLOCKED')), notes text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists intent_diffs_artboard_id_idx on intent_diffs (artboard_id); create index if not exists intent_diffs_status_idx on intent_diffs (status); create index if not exists intent_diffs_created_at_idx on intent_diffs (created_at desc); -- agent_sessions create table if not exists agent_sessions ( id uuid primary key default uuid_generate_v4(), artboard_id uuid not null references artboards (id) on delete cascade, diff_id uuid references intent_diffs (id) on delete set null, agent_type text not null check (agent_type in ('CURSOR','CLAUDE_CODE','GENERIC')), messages_jsonb jsonb not null default '[]', status text not null default 'ACTIVE' check (status in ('ACTIVE','COMPLETED','FAILED')), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists agent_sessions_artboard_id_idx on agent_sessions (artboard_id); create index if not exists agent_sessions_diff_id_idx on agent_sessions (diff_id); -- design_language_files create table if not exists design_language_files ( id uuid primary key default uuid_generate_v4(), workspace_id uuid not null references workspaces (id) on delete cascade, name text not null, schema_jsonb jsonb not null default '{}', version integer not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists design_language_files_workspace_id_idx on design_language_files (workspace_id); create index if not exists design_language_files_version_idx on design_language_files (workspace_id, version desc); -- team_members create table if not exists team_members ( id uuid primary key default uuid_generate_v4(), workspace_id uuid not null references workspaces (id) on delete cascade, user_id text not null, -- Clerk user ID role text not null default 'VIEWER' check (role in ('OWNER','DESIGNER','ENGINEER','PM','VIEWER')), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (workspace_id, user_id) ); create index if not exists team_members_workspace_id_idx on team_members (workspace_id); create index if not exists team_members_user_id_idx on team_members (user_id); -- ── Artboard ancestry (closure table) ──────────────────────────────────────── -- Maintained by triggers below. Stores every ancestor at any depth so hierarchy -- queries are O(1) lookups rather than recursive CTEs. create table if not exists artboard_ancestry ( artboard_id uuid not null references artboards (id) on delete cascade, ancestor_id uuid not null references artboards (id) on delete cascade, depth integer not null, primary key (artboard_id, ancestor_id) ); create index if not exists artboard_ancestry_ancestor_idx on artboard_ancestry (ancestor_id); -- ── Triggers: updated_at ───────────────────────────────────────────────────── create or replace function set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; create or replace trigger workspaces_updated_at before update on workspaces for each row execute function set_updated_at(); create or replace trigger artboards_updated_at before update on artboards for each row execute function set_updated_at(); create or replace trigger intent_diffs_updated_at before update on intent_diffs for each row execute function set_updated_at(); create or replace trigger agent_sessions_updated_at before update on agent_sessions for each row execute function set_updated_at(); create or replace trigger design_language_files_updated_at before update on design_language_files for each row execute function set_updated_at(); create or replace trigger team_members_updated_at before update on team_members for each row execute function set_updated_at(); -- ── Trigger: artboard ancestry maintenance ──────────────────────────────────── create or replace function maintain_artboard_ancestry() returns trigger language plpgsql as $$ begin -- Self-row (depth 0) insert into artboard_ancestry (artboard_id, ancestor_id, depth) values (new.id, new.id, 0) on conflict do nothing; -- Inherit all ancestors of the parent if new.parent_artboard_id is not null then insert into artboard_ancestry (artboard_id, ancestor_id, depth) select new.id, ancestor_id, depth + 1 from artboard_ancestry where artboard_id = new.parent_artboard_id on conflict do nothing; end if; return new; end; $$; create or replace trigger artboards_ancestry_insert after insert on artboards for each row execute function maintain_artboard_ancestry(); -- ── RPC: get_diffs_by_status ────────────────────────────────────────────────── -- Used by getDiffsByStatus() in origin-graph/queries.ts. -- Joins intent_diffs → artboards to scope by workspace_id. create or replace function get_diffs_by_status(p_workspace_id uuid, p_status text) returns setof intent_diffs language sql stable as $$ select d.* from intent_diffs d join artboards a on a.id = d.artboard_id where a.workspace_id = p_workspace_id and d.status = p_status order by d.created_at desc; $$; -- ── Row Level Security ──────────────────────────────────────────────────────── -- API routes use the service-role key (bypasses RLS). Policies below apply -- when the anon/browser key is used for direct-to-Supabase reads. -- All tables are locked down; only authenticated Postgres users may read. alter table workspaces enable row level security; alter table origins enable row level security; alter table artboards enable row level security; alter table intent_diffs enable row level security; alter table agent_sessions enable row level security; alter table design_language_files enable row level security; alter table team_members enable row level security; alter table artboard_ancestry enable row level security; -- Service role bypasses RLS automatically. For completeness, deny all anon. -- Add permissive policies when you add Supabase Auth integration. create policy "No anon access — workspaces" on workspaces for all to anon using (false); create policy "No anon access — origins" on origins for all to anon using (false); create policy "No anon access — artboards" on artboards for all to anon using (false); create policy "No anon access — intent_diffs" on intent_diffs for all to anon using (false); create policy "No anon access — agent_sessions" on agent_sessions for all to anon using (false); create policy "No anon access — design_language_files" on design_language_files for all to anon using (false); create policy "No anon access — team_members" on team_members for all to anon using (false); create policy "No anon access — artboard_ancestry" on artboard_ancestry for all to anon using (false);