Database Schema
Navigate Supabase database schema, 86+ migrations, and type definitions.
Overview
PostgreSQL schema via Supabase with RLS policies. See docs/database/SCHEMA.md.
Database Access
⚠️ CRITICAL: Must be in Railway shell for all database operations
# Verify environment first
echo $RAILWAY_ENVIRONMENT # Must be non-empty
# If empty, enter Railway shell:
railway shell
Common Queries:
# Quick introspection
psql "$DATABASE_URL" -c "\dt" # List all tables
psql "$DATABASE_URL" -c "\d users" # Describe users table
psql "$DATABASE_URL" -c "\d+ accounts" # Detailed table info with indexes
# Core tables
psql "$DATABASE_URL" -c "SELECT * FROM users LIMIT 5;"
psql "$DATABASE_URL" -c "SELECT * FROM accounts WHERE user_id = 'user_xxx';"
psql "$DATABASE_URL" -c "SELECT * FROM holdings WHERE account_id = 'acc_xxx';"
Migrations:
# Inside railway shell + supabase directory
cd supabase
supabase db push # Apply local migrations (see health check below)
supabase db diff -f new_migration_name # Generate migration from changes
See AGENTS.md "Database Access" section for complete guide.
Migrations
supabase/migrations/ - All 86+ migrations (sequential)
- Format:
YYYYMMDD_description.sql
- Key prefixes:
*clerk*, *plaid*, *eodhd*
End-to-end Checklist (schema changes)
Any time you change schema (tables, columns, indexes, RLS) or add Supabase migrations:
Work in Railway dev shell
- Confirm:
echo $RAILWAY_ENVIRONMENT is non-empty.
- Use the dev
DATABASE_URL and SUPABASE_PROJECT_ID.
Apply migrations to dev
cd supabase
supabase db push
- If this fails on old migrations, follow the health check flow below (registry repair or idempotent DDL) before proceeding.
Regenerate types and manifest
# From repo root, still in Railway shell
export SUPABASE_PROJECT_ID=klrrntdswlvjdqusahdk # or injected value
make schema:generate
- This must update:
supabase/types/database.types.ts
supabase/generated/schema_manifest.json
backend/schemas/generated/**
Verify schema parity
cd backend
PYTHONPATH=. poetry run python ../scripts/verify_generated_schemas.py
- This is the same check Tier 2 Auth Stub uses in CI.
- Fix any reported mismatches (missing columns, wrong nullability) before committing.
Commit everything together
- In a feature branch:
supabase/migrations/** changes
supabase/schemas/** changes
- Regenerated types and manifests
- Updated
backend/schemas/generated/**
- Do not split schema SQL and generated artifacts into separate feature branches; they must land atomically.
Migration Health Check (bd-k1c learnings)
Before adding or merging new migrations:
Verify registry vs schema
If db push fails on old migrations
- Do not hack the schema via ad‑hoc SQL.
- Instead, repair the registry or make old migrations idempotent:
- Option A (registry repair): mark older versions as applied in
supabase_migrations.schema_migrations (see supabase/scripts/fix_migration_registry_bd_k1c.sql for the bd‑k1c repair).
- Option B (idempotent migrations): wrap non‑idempotent DDL (e.g.
CREATE TRIGGER) in IF NOT EXISTS blocks so replaying them is safe.
- Re‑run
supabase db push after repair; only then add/merge new migrations.
New migrations (forward-only rule)
- Prefer
CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, and CREATE INDEX IF NOT EXISTS when possible to make replays safe.
- For new migrations you add from now on, use a unique timestamp prefix per file (e.g.
20251206152000_...).
- RPC Functions: ALWAYS provide default values for arguments (e.g.
filter jsonb DEFAULT '{}') to avoid signature mismatches with standard backends.
- Standard Scrape/Doc Schema:
raw_scrapes: MUST have storage_uri (text).
documents: MUST have source (text).
Dev/test-data migrations hygiene
- Dev/test-data migrations must live under
supabase/dev_migrations/, NOT supabase/migrations/
- Schema migrations (DDL, RLS, indexes, FKs) go in
supabase/migrations/
- Test-data seeding goes in
scripts/db-commands/ or supabase/dev_migrations/
- Do not use
supabase db push --include-all on historical migrations; treat it as debugging tool only
- See
supabase/dev_migrations/README.md for usage
Migration Registry Repair (When CLI Fails)
If supabase db push fails with "Remote migration versions not found" (Drift), do NOT run manual SQL in Dashboard. This creates a vicious cycle.
Fix:
- Ensure
DATABASE_URL is set in Railway (required for CLI).
- Run repair to sync registry with local files:
# In Railway shell
supabase migration repair --status applied <version_id>
# Or for batch:
supabase migration repair --status applied 20251129... 20251204...
- Then run
supabase db push for new migrations.
Schema Definitions
supabase/schemas/public/ - Table definitions
supabase/schemas/public/tables/ - Individual table files
Type Generation
supabase/types/database.types.ts - Generated TypeScript types
- Generate via:
supabase gen types typescript
Backend Types
backend/schemas/generated/ - Generated Python types (if any)
Scripts
scripts/db-commands/ - Database utilities
backend/migrations/versions/ - Alembic migrations (if used)
Key Tables and Recent Changes
Holdings Table (public.holdings)
Core columns:
id, account_id, security_id, quantity, cost_basis
created_at, updated_at, closed_at
Active vs Closed Holdings (bd-k1c.4):
- Active holdings:
closed_at IS NULL - current portfolio positions
- Closed positions:
closed_at IS NOT NULL, quantity conventionally 0
- Plaid pipeline soft-closes positions that disappear from broker snapshots (doesn't delete)
- Manual holdings are NOT auto-closed by provider sync
Index:
- Partial index
idx_holdings_closed_at ON holdings(closed_at) WHERE closed_at IS NULL for efficient active holdings queries
Current portfolio views: Filter with WHERE closed_at IS NULL
Holdings Snapshots Table (public.holdings_snapshots)
Purpose: Append-only time-series snapshots for historical portfolio analytics (bd-k1c.6)
Core columns:
snapshot_at (TIMESTAMPTZ) - snapshot time, typically daily at market close
user_id, account_id, security_id
quantity, cost_basis, market_value, price_source
Key constraints:
UNIQUE (snapshot_at, account_id, security_id) for idempotency
- Indexes on
(user_id, snapshot_at DESC), (account_id, snapshot_at DESC), (security_id, snapshot_at DESC)
Relationship to holdings:
- Snapshots derived from active holdings + price data
- Snapshot job:
backend/scripts/create_holdings_snapshot.py
Provider Security Mappings (public.provider_security_mappings)
Purpose: Map provider security IDs to canonical securities
Natural key (bd-k1c.3):
UNIQUE (brokerage_connection_id, provider_security_id)
provider_security_id VARCHAR(255) - stable provider-side identifier (e.g., Plaid security_id)
provider_payload JSONB - retained for audit, NOT in uniqueness constraint
Index:
idx_provider_security_mappings_provider_security_id for fast lookups
Used by:
RawDataService.get_existing_security_mapping
SecurityResolver._link_provider_mapping (upserts on natural key)
Recent bd-k1c Changes
The bd-k1c epic (Plaid portfolio pipeline hardening) introduced several schema enhancements:
- Holdings soft-close semantics (
closed_at column) - distinguishes active vs closed positions
- Time-series snapshots (
holdings_snapshots table) - enables historical analytics
- Provider mapping refinement (
provider_security_id natural key) - more robust brokerage integrations
See docs/bd-k1c/EPIC_OVERVIEW.md for full context and child features.
Documentation
- Internal:
docs/database/SCHEMA.md
Related Areas
- See
context-clerk-integration for RLS patterns
- See
context-plaid-integration for plaid_prices table and provider mappings
- See
context-symbol-resolution for securities table
- See
context-portfolio for holdings views and analytics
1---2name: context-database-schema3description: Supabase PostgreSQL schema management, 86+ migrations, RLS policies, and type generation. Handles table creation, schema changes, migrations, foreign key constraints, and migration workflows. Use when working with database schema, migrations, data modeling, or type definitions, or when user mentions database changes, table modifications, schema updates, migration failures, "relation does not exist" errors, foreign key issues, Supabase schema operations, users table, accounts table, or holdings table.4---5
6# Database Schema
7
8Navigate Supabase database schema, 86+ migrations, and type definitions.
9
10## Overview
11
12PostgreSQL schema via Supabase with RLS policies. See `docs/database/SCHEMA.md`.
13
14## Database Access
15
16**⚠️ CRITICAL: Must be in Railway shell for all database operations**
17
18```bash
19# Verify environment first
20echo $RAILWAY_ENVIRONMENT # Must be non-empty
21
22# If empty, enter Railway shell:
23railway shell
24```
25
26**Common Queries:**
27
28```bash
29# Quick introspection
30psql "$DATABASE_URL" -c "\dt" # List all tables
31psql "$DATABASE_URL" -c "\d users" # Describe users table
32psql "$DATABASE_URL" -c "\d+ accounts" # Detailed table info with indexes
33
34# Core tables
35psql "$DATABASE_URL" -c "SELECT * FROM users LIMIT 5;"
36psql "$DATABASE_URL" -c "SELECT * FROM accounts WHERE user_id = 'user_xxx';"
37psql "$DATABASE_URL" -c "SELECT * FROM holdings WHERE account_id = 'acc_xxx';"
38```
39
40**Migrations:**
41
42```bash
43# Inside railway shell + supabase directory
44cd supabase
45supabase db push # Apply local migrations (see health check below)
46supabase db diff -f new_migration_name # Generate migration from changes
47```
48
49**See AGENTS.md "Database Access" section for complete guide.**
50
51## Migrations
52
53- `supabase/migrations/` - All 86+ migrations (sequential)
54- Format: `YYYYMMDD_description.sql`
55- Key prefixes: `*clerk*`, `*plaid*`, `*eodhd*`
56
57### End-to-end Checklist (schema changes)
58
59Any time you change schema (tables, columns, indexes, RLS) or add Supabase migrations:
60
611. **Work in Railway dev shell**
62 - Confirm: `echo $RAILWAY_ENVIRONMENT` is non-empty.
63 - Use the dev `DATABASE_URL` and `SUPABASE_PROJECT_ID`.
64
652. **Apply migrations to dev**
66 ```bash
67 cd supabase
68 supabase db push
69 ```
70 - If this fails on **old** migrations, follow the health check flow below (registry repair or idempotent DDL) before proceeding.
71
723. **Regenerate types and manifest**
73 ```bash
74 # From repo root, still in Railway shell
75 export SUPABASE_PROJECT_ID=klrrntdswlvjdqusahdk # or injected value
76 make schema:generate
77 ```
78 - This must update:
79 - `supabase/types/database.types.ts`
80 - `supabase/generated/schema_manifest.json`
81 - `backend/schemas/generated/**`
82
834. **Verify schema parity**
84 ```bash
85 cd backend
86 PYTHONPATH=. poetry run python ../scripts/verify_generated_schemas.py
87 ```
88 - This is the same check Tier 2 Auth Stub uses in CI.
89 - Fix any reported mismatches (missing columns, wrong nullability) before committing.
90
915. **Commit everything together**
92 - In a feature branch:
93 - `supabase/migrations/**` changes
94 - `supabase/schemas/**` changes
95 - Regenerated types and manifests
96 - Updated `backend/schemas/generated/**`
97 - Do **not** split schema SQL and generated artifacts into separate feature branches; they must land atomically.
98
99### Migration Health Check (bd-k1c learnings)
100
101Before adding or merging new migrations:
102
1031. **Verify registry vs schema**
104 - Run in Railway shell:
105 ```bash
106 cd supabase
107 supabase db push
108 ```
109 - If it fails on **old** migrations (tables/triggers already exist), it means the schema was initialized by `golden_schema.sql` / `all_migrations.sql` / manual SQL and the migration registry is behind.
110
1112. **If db push fails on old migrations**
112 - Do **not** hack the schema via ad‑hoc SQL.
113 - Instead, repair the registry or make old migrations idempotent:
114 - Option A (registry repair): mark older versions as applied in `supabase_migrations.schema_migrations` (see `supabase/scripts/fix_migration_registry_bd_k1c.sql` for the bd‑k1c repair).
115 - Option B (idempotent migrations): wrap non‑idempotent DDL (e.g. `CREATE TRIGGER`) in `IF NOT EXISTS` blocks so replaying them is safe.
116 - Re‑run `supabase db push` after repair; only then add/merge new migrations.
117
1183. **New migrations (forward-only rule)**
119 - Prefer `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, and `CREATE INDEX IF NOT EXISTS` when possible to make replays safe.
120 - For **new migrations you add from now on**, use a **unique timestamp prefix** per file (e.g. `20251206152000_...`).
121 - **RPC Functions**: ALWAYS provide default values for arguments (e.g. `filter jsonb DEFAULT '{}'`) to avoid signature mismatches with standard backends.
122 - **Standard Scrape/Doc Schema**:
123 - `raw_scrapes`: MUST have `storage_uri` (text).
124 - `documents`: MUST have `source` (text).
125
1264. **Dev/test-data migrations hygiene**
127 - **Dev/test-data migrations** must live under `supabase/dev_migrations/`, NOT `supabase/migrations/`
128 - Schema migrations (DDL, RLS, indexes, FKs) go in `supabase/migrations/`
129 - Test-data seeding goes in `scripts/db-commands/` or `supabase/dev_migrations/`
130 - Do not use `supabase db push --include-all` on historical migrations; treat it as debugging tool only
131 - See `supabase/dev_migrations/README.md` for usage
132
133### Migration Registry Repair (When CLI Fails)
134
135If `supabase db push` fails with "Remote migration versions not found" (Drift), do **NOT** run manual SQL in Dashboard. This creates a vicious cycle.
136
137**Fix:**
1381. Ensure `DATABASE_URL` is set in Railway (required for CLI).
1392. Run repair to sync registry with local files:
140 ```bash
141 # In Railway shell
142 supabase migration repair --status applied <version_id>
143 # Or for batch:
144 supabase migration repair --status applied 20251129... 20251204...
145 ```
1463. Then run `supabase db push` for new migrations.
147
148## Schema Definitions
149
150- `supabase/schemas/public/` - Table definitions
151- `supabase/schemas/public/tables/` - Individual table files
152
153## Type Generation
154
155- `supabase/types/database.types.ts` - Generated TypeScript types
156- Generate via: `supabase gen types typescript`
157
158## Backend Types
159
160- `backend/schemas/generated/` - Generated Python types (if any)
161
162## Scripts
163
164- `scripts/db-commands/` - Database utilities
165- `backend/migrations/versions/` - Alembic migrations (if used)
166
167## Key Tables and Recent Changes
168
169### Holdings Table (`public.holdings`)
170
171**Core columns:**
172- `id`, `account_id`, `security_id`, `quantity`, `cost_basis`
173- `created_at`, `updated_at`, `closed_at`
174
175**Active vs Closed Holdings (bd-k1c.4):**
176- **Active holdings**: `closed_at IS NULL` - current portfolio positions
177- **Closed positions**: `closed_at IS NOT NULL`, `quantity` conventionally `0`
178- Plaid pipeline **soft-closes** positions that disappear from broker snapshots (doesn't delete)
179- Manual holdings are NOT auto-closed by provider sync
180
181**Index:**
182- Partial index `idx_holdings_closed_at ON holdings(closed_at) WHERE closed_at IS NULL` for efficient active holdings queries
183
184**Current portfolio views**: Filter with `WHERE closed_at IS NULL`
185
186### Holdings Snapshots Table (`public.holdings_snapshots`)
187
188**Purpose**: Append-only time-series snapshots for historical portfolio analytics (bd-k1c.6)
189
190**Core columns:**
191- `snapshot_at` (TIMESTAMPTZ) - snapshot time, typically daily at market close
192- `user_id`, `account_id`, `security_id`
193- `quantity`, `cost_basis`, `market_value`, `price_source`
194
195**Key constraints:**
196- `UNIQUE (snapshot_at, account_id, security_id)` for idempotency
197- Indexes on `(user_id, snapshot_at DESC)`, `(account_id, snapshot_at DESC)`, `(security_id, snapshot_at DESC)`
198
199**Relationship to holdings:**
200- Snapshots derived from active holdings + price data
201- Snapshot job: `backend/scripts/create_holdings_snapshot.py`
202
203### Provider Security Mappings (`public.provider_security_mappings`)
204
205**Purpose**: Map provider security IDs to canonical securities
206
207**Natural key (bd-k1c.3):**
208- `UNIQUE (brokerage_connection_id, provider_security_id)`
209- `provider_security_id VARCHAR(255)` - stable provider-side identifier (e.g., Plaid `security_id`)
210- `provider_payload JSONB` - retained for audit, NOT in uniqueness constraint
211
212**Index:**
213- `idx_provider_security_mappings_provider_security_id` for fast lookups
214
215**Used by:**
216- `RawDataService.get_existing_security_mapping`
217- `SecurityResolver._link_provider_mapping` (upserts on natural key)
218
219## Recent bd-k1c Changes
220
221The **bd-k1c epic** (Plaid portfolio pipeline hardening) introduced several schema enhancements:
222
223- **Holdings soft-close semantics** (`closed_at` column) - distinguishes active vs closed positions
224- **Time-series snapshots** (`holdings_snapshots` table) - enables historical analytics
225- **Provider mapping refinement** (`provider_security_id` natural key) - more robust brokerage integrations
226
227See `docs/bd-k1c/EPIC_OVERVIEW.md` for full context and child features.
228
229## Documentation
230
231- **Internal**: `docs/database/SCHEMA.md`
232
233## Related Areas
234
235- See `context-clerk-integration` for RLS patterns
236- See `context-plaid-integration` for plaid_prices table and provider mappings
237- See `context-symbol-resolution` for securities table
238- See `context-portfolio` for holdings views and analytics