Declarative schema workflow, generated migrations, RLS placement, seeds, and generated types.
CentraKit uses Supabase declarative schema management.
supabase/schemas/ is the source of truth for database structure. Do not create or write migration files directly during normal schema work.
supabase/schemas/**/*.sql.pnpm supabase:diff -f <migration_name>, which runs pg-delta (supabase db schema declarative sync --no-apply).Writes that must commit together, such as sending a quote with its party snapshot, published version and status history, go in one security invoker function that the service calls once, because each Data API request runs in its own transaction.
For rollbacks, revert declarative schema files and generate a new forward migration instead of rewriting history.
supabase/schemas/ holds extensions.sql and shared/ (cross-domain types, number sequences and schema grants), then one folder per domain: tenant, identity, crm, catalog, work, people, commerce, expenses, conversations, ai, automation, platform and portal.
Inside a domain folder, each table has one file named after the table, such as commerce/document_parties.sql. The file holds the table with its indexes, comments, triggers, audit trigger, RLS and policies, so everything about a table is in one place. Enums live in <domain>/types.sql, functions in <domain>/functions/<area>.sql, and bucket policies in <domain>/storage.sql. better_supabase/ is generated by better-supabase, and the helper and hook files in identity/permdock/ by PermDock.
pg-delta orders the statements by their dependencies, so supabase/config.toml has no schema_paths list and file names do not set the run order. Define each function in one file only. After any move or rename, pnpm supabase:diff must report no schema changes, and CI fails on drift.
Every table must have RLS enabled. Its policies live in the table file; pg-delta creates the helper functions a policy calls before the policy.
Tenant-table policies use the init-plan form organization_id in (select permdock.permitted_organization_ids('<key>')), which Postgres evaluates once per statement (a hashed SubPlan) instead of calling a helper for every candidate row. A permission whose grants are split by condition is named by its permission key through permdock.permitted_organization_ids_by_permission('<permission>'). Platform tables use (select permdock.permdock_has('platform.<...>')), and chat rows use thread_id in (select public.accessible_chat_thread_ids()). The helpers are generated from the PermDock policy; see RBAC.
A few policies take the permission key from the row: the lifecycle policies in supabase/schemas/automation/workflow_lifecycles.sql check each subject type in the init-plan form, and the contextual artifact policies in supabase/schemas/ai/ai_contextual_artifacts.sql pass the row's origin_permission to permdock.permitted_organization_ids_by_permission.
Seed data lives under supabase/seeds/. After schema changes, regenerate Supabase TypeScript types for both public and graphql_public schemas so generated code stays complete.
Last updated on