Skip to main content

Architecture

Request flow

Routes never talk to the database directly. Every protected route requires a verified AuthUser. User-scoped services (profile, account) receive the verified AuthUser and filter queries by user.id; shared discovery and analytics services (discovery, velocity) return typed Pydantic models over discovery.companies or app.signals_timeseries.

Modules

  • app/api/v1/ — HTTP surface: account, profile, organizations, velocity (metrics), auth (dependency), and api (router aggregation).
  • app/core/ — cross-cutting setup: config, logging, metrics, middleware, rate limiting.
  • app/models/ — SQLModel mappings for the Supabase app schema (app.profiles).
  • app/schemas/ — Pydantic request/response models (auth, organizations, profile, velocity).
  • app/services/ — business logic, repositories, and clients: Postgres (db_engine, profile, profile_repo, account, discovery, discovery_repo, velocity).
  • app/temporal/ — boilerplate client and worker; add workflows here when needed.
  • app/utils/auth.py — Supabase JWKS token verification.

Design decisions

  • Users live in Supabase Auth. The API stores only auth.users.id references.
  • Backend-only schema. app is service-role only: RLS enabled, no policies.
  • Postgres as single operational and analytics store. Postgres handles the transactional graph (entities, affiliations, discrete entity_signals) as well as append-only metric observations (app.signals_timeseries) used for rolling growth velocity.
  • Discovery view, not raw graph. The read endpoints query discovery.companies (a denormalized view), never the normalized tables, so the frontend gets typed contracts and never has to join or handle merged_into_id.
  • Keyset pagination. List endpoints paginate with an opaque next_cursor over a stable sort key, not LIMIT/OFFSET, so deep pages stay index-friendly.
  • One async engine. app/services/db_engine.py owns the pooled connection used by all Postgres repositories and the health check.
  • Errors are 400/401/404/503. Missing or foreign rows are indistinguishable from absent rows, so ids cannot be probed; invalid query parameters return 400, and unavailable upstream dependencies (JWKS, Auth Admin API) return 502/503.