Skip to content

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>:

Authorization: Bearer dev:demo@navase.example

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_URL is set → verify against the project JWKS (ES256/RS256).
  • Else → verify HS256 against SUPABASE_JWT_SECRET.
  • Both require exp and sub, 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):

  1. If a User with that id exists → return it.
  2. Else if a User with that email exists (an agent an admin pre-added) → attach this id to that row and return it.
  3. Else → create a new Company named "<email-prefix>'s Workspace", create the User as its admin, run seed_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.id from get_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.