PostgreSQL and Supabase database patterns. Use when writing SQL migrations, creating tables, modifying schemas, or working with data access code. Covers RLS with is_record_owner(), table structure, migrations, JSONB, indexes, timestamps, and schema documentation.
| ID | Rule | Enforcement |
|---|---|---|
| A3 | Supabase REST+RLS for user CRUD; psycopg for framework ops | Reviewer |
| A8 | All tables: RLS enabled + is_record_owner() policy | validate-patterns.sh BLOCKS |
| A9 | UUID FKs with ON DELETE, never agent_name TEXT | validate-patterns.sh BLOCKS |
| A10 | Entity "foo" → table foos, migration create_foos.sql | Reviewer |
For full rationale on any principle: .claude/skills/architecture-principles/reference.md
All data lives in one PostgreSQL database on Supabase. No SQLite, Redis, MongoDB, or additional data stores.
Before any database work, read supabase/schema.sql — this is the single source of truth for the current production DDL. It contains all tables, functions, indexes, RLS policies, and comments in one file.
If the file is missing or stale, regenerate it:
./scripts/dump-schema.sh
Always check the existing schema before creating new tables or columns. The schema file prevents duplicating existing structures or misunderstanding current column types/constraints.
Before writing database code, verify:
is_record_owner() policygen_random_uuid()user_id FK to auth.users(id) ON DELETE CASCADEcreated_at and updated_at TIMESTAMPTZ columnsupdated_atsupabase/migrations/agent_id UUID FK → agent_configurations(id), NOT agent_name TEXTCREATE TABLE example_table (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
CONSTRAINT unique_user_example UNIQUE (user_id, name)
);
ALTER TABLE example_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable full access for record owners on example_table"
ON example_table FOR ALL
USING (public.is_record_owner(user_id))
WITH CHECK (public.is_record_owner(user_id));
CREATE INDEX idx_example_table_user ON example_table(user_id);
CREATE TRIGGER update_example_table_updated_at
BEFORE UPDATE ON example_table
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE example_table IS 'Description of what this table stores';
Migration files are named YYYYMMDDHHMMSS_descriptive_name.sql. When agents work in parallel worktrees, timestamp collisions cause merge failures.
Rules:
ls supabase/migrations/ | grep -oP '^\d{14}' | sort | tail -1
Then increment by 1.validate-patterns.sh hook will warn if a collision is detected against the main repo.ON CONFLICT requires a real UNIQUE constraint (not partial unique indexes). Use select-then-insert if needed.ON CONFLICT DO UPDATE must include type — Upserts on the tools table must include type = EXCLUDED.type in the SET clause. Omitting it leaves stale type values, silently breaking tool loading. The validate-patterns.sh hook enforces this.agent_tools.is_active reactivation — INSERT ON CONFLICT DO NOTHING won't reactivate a deactivated row. Use UPDATE ... SET is_active = true WHERE is_active = false instead. Discovered in SPEC-019 when create_tasks link was silently missing.service_role key used by backend services bypasses all RLS policies. User data isolation is enforced at the API layer via UserScopedClient (SPEC-017), with RLS as defense-in-depth. Never use raw get_supabase_client in services — use get_user_scoped_client or get_system_client.For full patterns with examples (RLS testing, JSONB config, index strategy, data access in Python), see reference.md.