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:
| Check | Requirement |
|---|---|
| Signature | Valid against the project's published signing keys, using ES256 or RS256. Unsigned and other algorithms are rejected |
| Expiry | The token has not expired |
| Issuer | <SUPABASE_URL>/auth/v1 |
| Audience | authenticated |
| Role claim | authenticated; this rejects anon and service_role tokens even though they are valid project tokens |
| Subject | sub 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.
| Surface | Relationship resolved from the verified subject | Without it |
|---|---|---|
| Professional routes | The application user with a professional enrollment | Access denied. Nothing is created or enrolled |
| Client routes | The client account bound to the subject, with verified contact | Access denied with contact_required or account_required. The session stays valid |
| Client onboarding status | The client account bound to the subject, with or without contact | Reported as not found |
Website routes (/users, /media) | The application user, created on first use | Unchanged generic behavior |
- Professional enrollment is explicit. Generic user resolution for website
routes can create a
usersrow, 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 explicitPOST /users/me/professional-enrollmentfrom the website. That request creates the user if needed and records an enrollment with sourcewebsite. It is idempotent: an existing enrollment keeps its original source and time, which the response reports asenrolledAt. - 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_requiredorready, 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
usersrow 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
usersrows that stored a previous-provider id do not match any Supabase user and are not remapped, including Telegram professionals keyedtelegram:<id>. - The Supabase Data API stays disabled and the API does not use
@supabase/supabase-js. Application data is reached only throughpg; 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.