Database Patterns
Principles That Apply
| 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
Core Rule
All data lives in one PostgreSQL database on Supabase. No SQLite, Redis, MongoDB, or additional data stores.
Step 0: Read the Current Schema
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.
Quick Checklist
Before writing database code, verify:
- RLS enabled with
is_record_owner()policy - UUID primary key with
gen_random_uuid() -
user_idFK toauth.users(id) ON DELETE CASCADE -
created_atandupdated_atTIMESTAMPTZ columns - Auto-update trigger on
updated_at - Schema change is in a migration file under
supabase/migrations/ - Indexes on user_id and common query columns
- Agent references use
agent_id UUID FK→agent_configurations(id), NOTagent_name TEXT - JSONB for flexible config (not key-value tables)
- Table and column COMMENT statements
- snake_case naming throughout
Table Template (Quick Copy)
CREATE 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 Timestamp Uniqueness
Migration files are named YYYYMMDDHHMMSS_descriptive_name.sql. When agents work in parallel worktrees, timestamp collisions cause merge failures.
Rules:
- Never invent a prefix. Use the one assigned by the orchestrator in the task contract.
- If no prefix was assigned, derive the next available one from existing migrations:
Then increment by 1.ls supabase/migrations/ | grep -oP '^\d{14}' | sort | tail -1 - The
validate-patterns.shhook will warn if a collision is detected against the main repo.
Key Gotchas
- PostgREST upsert — Supabase PostgREST
ON CONFLICTrequires a real UNIQUE constraint (not partial unique indexes). Use select-then-insert if needed. - PostgREST function overloads — PostgREST can't disambiguate overloaded SQL functions (same name, different arg counts). When adding a new overload of a function called via PostgREST, drop the old signature in the same migration. Leaving both causes PGRST203 errors at runtime.
ON CONFLICT DO UPDATEmust includetype— Upserts on thetoolstable must includetype = EXCLUDED.typein the SET clause. Omitting it leaves staletypevalues, silently breaking tool loading. Thevalidate-patterns.shhook enforces this.agent_tools.is_activereactivation —INSERT ON CONFLICT DO NOTHINGwon't reactivate a deactivated row. UseUPDATE ... SET is_active = true WHERE is_active = falseinstead. Discovered in SPEC-019 whencreate_taskslink was silently missing.- Service-role key bypasses RLS — The
service_rolekey used by backend services bypasses all RLS policies. User data isolation is enforced at the API layer viaUserScopedClient(SPEC-017), with RLS as defense-in-depth. Never use rawget_supabase_clientin services — useget_user_scoped_clientorget_system_client.
Detailed Reference
For full patterns with examples (RLS testing, JSONB config, index strategy, data access in Python), see reference.md.