Database & Migrations
The schema lives insupabase/migrations/ as plain SQL. apps/api only maps
existing tables (app/models/db.py) and views (app/services/discovery_repo.py);
nothing in the app creates or alters the schema.
Tables
Users themselves live in Supabase Auth (
auth.users). User-owned rows reference
auth.users.id with on delete cascade, so deleting the auth user removes their data.
Account deletion goes through the Supabase Auth Admin API
(DELETE /auth/v1/admin/users/{id}) using the service role key. That removes the
auth.users row and cascades to app.* rows via the foreign key, and it keeps the
API’s Postgres role from needing write access to auth.users.
Discovery layer
The read endpoints never query the normalized graph directly. Thediscovery
schema exposes one denormalized view, discovery.companies, that folds the graph
joins, merged_into_id filtering, and 180-day signal aggregation into a single table.
The API exposes it as typed, read-only resources:
These are plain
SELECTs against the view — typed Pydantic models all the way
through — not a raw-SQL query endpoint. The list endpoint paginates with a
keyset cursor (next_cursor), not LIMIT/OFFSET, so deep pages stay
index-friendly as the graph grows.
Growth velocity timeseries
Metric observations are appended toapp.signals_timeseries in Postgres. The API computes rolling growth velocity directly from this table:
Supported
metric_type values: github_stars, github_forks, commit_count,
headcount, open_jobs_count, website_traffic_rank, twitter_followers,
devpost_likes. The query aggregates the earliest and latest values per organization in the window to compute absolute growth and growth percentage.
Access model
appis backend-only:anon/authenticatedare revoked, RLS is enabled with no policies (default deny), and onlyservice_rolehas grants.apps/apiconnects as thepostgresrole, which bypasses RLS. User-owned queries (app.profiles) filter by the verified user id; discovery queries read from thediscovery.companiesview.
Applying migrations
Connecting
PointPOSTGRES_* at the Supabase session pooler (port 5432), not the
transaction pooler (6543), which breaks prepared statements. SUPABASE_URL must
be set for token verification. See Configuration.