Request lifecycle

Page request

  1. middleware.ts runs for every non-excluded path. It short-circuits /.well-known, /icons/ and /assets/, then reads the Supabase session from cookies.

  2. If a session exists, the user’s email is checked against isAllowedPayabilityEmail() — anything outside payability.com (or a subdomain) is signed out and redirected to /login?error=forbidden_domain.

  3. Unauthenticated requests to anything other than /login, /signup, /auth/callback, /docs or /api are redirected to /login?redirectedFrom=<path>.

  4. Authenticated requests to /login or /signup are redirected to /.

  5. The dashboard layout renders inside WorkspaceGate, which blocks rendering until GET /api/me resolves and the user has at least one workspace with an active selection.

Excluded from middleware entirely (see the matcher in middleware.ts): _next/static, _next/image, favicon.ico, icons, assets, images, fonts, supabase/functions, api/webhooks, .well-known.

API request

sequenceDiagram
  participant C as Client (React Query hook)
  participant R as Route handler
  participant A as resolveSubscription()
  participant S as Service
  participant D as Repository
  participant P as PostgreSQL

  C->>R: fetch("/api/…", { cache: "no-store" })
  R->>A: resolve user + active workspace
  A->>P: Profile + SubscriptionUser lookup
  A-->>R: { userId, subscriptionId }
  R->>R: Zod parse body / query
  R->>S: delegate
  S->>D: repository call
  D->>P: Prisma query (scoped by subscriptionId)
  P-->>D: rows
  D-->>S: models
  S-->>R: domain result
  R-->>C: snake_case JSON
  C->>C: Schema.parse(response)

resolveSubscription() also stamps Sentry with the user id, email and subscriptionId tag, so any error captured later in the request is already attributed to a tenant.

Webhook request

Provider webhooks bypass middleware (api/webhooks is excluded from the matcher) and authenticate themselves:

  1. Read the raw body as text — signature verification needs the exact bytes.

  2. Verify the provider signature where one exists (Svix for Resend, HMAC for HubSpot, optional bearer/X-Nooks-Signature for Nooks).

  3. Upsert a WebhookEvent row, unique on (source, eventId). A duplicate delivery is a no-op.

  4. Dispatch to the provider’s registry, which instantiates the handler for that event type.

  5. Return 200 with { received: true } even if the handler failed — the raw event is stored and can be replayed, and a 500 would only trigger provider retries of work that may have partially succeeded.

See Events for the dispatchers and handlers.

Machine request

pg_cron, Supabase Edge Functions and other internal services call broadcast and cron endpoints with Authorization: Bearer <INTERNAL_API_SECRET>, verified by verifyInternalSecret() in lib/auth/internal-secret.ts. It fails closed: if the secret is unset, every request is rejected.

The MCP endpoint uses its own shared secret (MCP_API_SECRET) plus an explicit x-subscription-id header instead of a cookie — see MCP tool server.