Skip to main content

Authentication

Protected API operations act on behalf of a signed-in user. The API proves who the caller is by verifying a Supabase Auth access token, then hands only the verified Supabase Auth user id to the users slice. Supabase Auth proves identity; the application user owns product data.

This page explains the behavior. Endpoint paths and status codes are defined by the Fastify route schemas. The website side is described in Website sign-in.

Who calls and what they send​

A client sends Authorization: Bearer <access_token>, where the token is the access token of a Supabase Auth session. Session cookies are not accepted. A missing header, a malformed header, or any token that fails verification gets the same generic authentication failure, so a response never reveals why a token was rejected.

What the API checks​

The API verifies the token locally with jose. It does not call Supabase on each request and holds no Supabase secret. A token is accepted only when all of these hold:

CheckRequirement
SignatureValid against the project's published signing keys, using ES256 or RS256. Unsigned and other algorithms are rejected
ExpiryThe token has not expired
Issuer<SUPABASE_URL>/auth/v1
Audienceauthenticated
Role claimauthenticated; this rejects anon and service_role tokens even though they are valid project tokens
Subjectsub is a non-empty string

The signing keys are fetched lazily from <SUPABASE_URL>/auth/v1/.well-known/jwks.json and cached. Startup validates only the shape of SUPABASE_URL; the API still starts if Supabase is unreachable, and protected requests fail until the keys can be fetched.

Unlike the previous provider, the token carries no browser origin, so there is no per-token origin check. Which browser origins may call the API is controlled by the existing CORS allowlist (CORS_ORIGIN).

Handoff to the users slice​

After verification, the shared authentication boundary exposes the sub value as the provider user id. The users slice creates one application user for that id on first use and later resolves the same user. The id is stored as the opaque, unique identity_provider_id, with no foreign key into Supabase's auth.users. The application's own UUID, not the Supabase id, owns profile and media data. See Core user and media schema.

Authorization after verification​

A valid Supabase session proves only who the person is. Each API surface then resolves its own application relationship, and the login channel, token metadata, or a client-supplied account type never grants access.

SurfaceRelationship resolved from the verified subjectWithout it
Professional routesThe application user with a professional enrollmentAccess denied. Nothing is created or enrolled
Client routesThe client account bound to the subject, with verified contactAccess denied with contact_required or account_required. The session stays valid
Client onboarding statusThe client account bound to the subject, with or without contactReported as not found
Website routes (/users, /media)The application user, created on first useUnchanged generic behavior
  • Professional enrollment is explicit. Generic user resolution for website routes can create a users row, but it never enrolls anyone. A professional is enrolled through a verified launch of the professional Mini App, which channel authentication binds as described below, or by an explicit POST /users/me/professional-enrollment from the website. That request creates the user if needed and records an enrollment with source website. It is idempotent: an existing enrollment keeps its original source and time, which the response reports as enrolledAt.
  • Client contact is onboarding state. A Telegram client gets a Supabase session and an incomplete client account before sharing a phone. Every existing client account operation, including booking as a signed-in client, stays unavailable until a verified contact arrives. The onboarding status reports contact_required or ready, and bot write permission separately.
  • One person, separate relationships. The same Supabase subject can hold a professional enrollment and a client account. Each surface resolves only its own account ID and enforces ownership with it.

Old professional and client JWTs issued by the API and channel identity assertions are rejected on end-user routes, even when correctly signed. Public booking and management-link routes stay available without a token.

:::caution Migration in progress The API accepts only Supabase sessions on professional and client routes, and both Mini Apps obtain them through the API's channel authentication routes. The old Telegram exchange route, bot webhook and session secrets are gone. Deploy it together with the channel authentication cutover, which has not yet been rehearsed or run against hosted Supabase. :::

Binding channel identities​

Channel authentication binds a verified channel identity to these relationships in-process, through the users and client-accounts slices' exported use cases. No route accepts a binding, a verified contact or a permission from a caller, whatever credential it presents.

  • Binding. A Supabase subject maps to at most one users row and one client account. A professional-app launch can create and enroll the professional. A client-app launch can create an incomplete client account. Each step is idempotent, so retries after a partial failure converge.
  • Facts. Only verified bot updates set bot write permission. A client account's verified phone comes from a verified bot contact update or, for a website client account, from the phone Supabase Auth confirmed for the caller. Only each bot's latest fact is applied, so an older update never overwrites newer state, and the client and professional bots have separate permission state. Professional-bot facts change nothing in the API today. A fact that arrives before its client account exists is applied once the account is created.

Channel credential​

The same resolved application user is the only audience a website credential may name. The users slice signs a five-minute token whose subject is that user's UUID, so the website can join that user's private channel and no other. A caller who fails the checks above receives no token. The publish secret is not part of the credential. See Realtime event channels and Live user events.

Consequences of the provider change​

  • Tokens issued by the previous provider are rejected. There is no period in which both providers are accepted.
  • Existing users rows that stored a previous-provider id do not match any Supabase user and are not remapped, including Telegram professionals keyed telegram:<id>.
  • The Supabase Data API stays disabled and the API does not use @supabase/supabase-js. Application data is reached only through pg; see Database connection.

Configuration​

The API requires SUPABASE_URL. Channel authentication's routes are mounted only when CHANNEL_AUTH_ISSUER_URL is set. See Environment variables.