Auth and multi-tenancy¶
All of this lives in backend/app/auth.py.
Two auth modes¶
Controlled by settings.dev_auth (config.py, env-driven).
Dev auth (local, and the current hosted demo)¶
The token is literally dev:<email>:
get_current_user splits off the email, derives a stable user id
(uuid5(NAMESPACE_DNS, "dev-auth:<email>")), and calls _get_or_create_user.
No signature, no expiry — it is trusted as-is. Fine for a demo, not for real
tenants.
Supabase JWT (production)¶
_verify_supabase_token:
- If
SUPABASE_URLis set → verify against the project JWKS (ES256/RS256). - Else → verify
HS256againstSUPABASE_JWT_SECRET. - Both require
expandsub, audience"authenticated".
The user id is the Supabase sub; the email comes from the token.
First sign-in creates the tenant¶
_get_or_create_user(db, user_id, email):
- If a
Userwith that id exists → return it. - Else if a
Userwith that email exists (an agent an admin pre-added) → attach this id to that row and return it. - Else → create a new
Companynamed"<email-prefix>'s Workspace", create theUseras its admin, runseed_demo_data(db, company, user)(two sample verticals), commit.
So: the first person to sign in with a given email becomes an admin of a fresh workspace. Agents only exist because an admin added them by email on the Team page; they get matched on their first sign-in via step 2.
Dependencies you'll use in routers¶
| Dependency | Effect |
|---|---|
get_current_user |
resolves the token → User (creating on first sign-in) |
require_admin |
403 unless role in ("admin", "super_admin") |
require_super_admin |
403 unless role == "super_admin" |
get_company(db, user) |
the user's Company, or 400 if none |
require_premium_company(db, user) |
get_company + 402 if not is_premium |
The tenancy rule¶
There is no database-level isolation. Every query must:
- filter on
Contact.company_id == company.id(etc.), and - take
company.idfromget_company(db, user)— never from the request body or a query param.
A new endpoint that skips either of those is a cross-tenant data leak. When
reviewing a change, check that every db.query(...) in a router filters by
company_id and that the id's provenance is the authenticated user.
Roles vs. the paywall — orthogonal¶
Role gating (require_admin) and the paywall (require_premium_company) are
independent. An admin on a free workspace can reach the Settings page but
every save returns 402. An agent on a premium workspace can log follow-ups
but can't open Sequences. Apply whichever (or both) a route needs.