# Authentication

This page covers authentication for the users of **your** application: sign-up, sign-in, identity providers, two-factor authentication, emails, and the keys that open your project's API. It is compatible with the `supabase-js` client: your application changes the address and the key, not its code. Members signing in to the Bastide console is a different topic (see [Account](account.md) and [Single sign-on](sso.md)).

## Concepts

- **Users** live in your database, in the `auth` schema. Your policies recognise them with `auth.uid()` (see [Data security](data-security.md)).
- **The access token** is a project JWT, signed with the project's secret, valid for one hour; the REST API checks it without calling the platform. A global sign-out makes it unusable from the next request.
- **The session** lasts thirty days. Its refresh token rotates on every use; an already consumed token that comes back revokes the whole session, because it signals theft.
- **The password** is 12 to 256 characters long (in code points).
- **The paths** are Supabase's, under the project address: `https://<identifier>.donnees.bastide.app/auth/v1/…` (see [Database](database.md)).

## What `supabase-js` gets

Measured with `@supabase/supabase-js` 2.117.2, the published package:

| Call | Request | Verdict |
| --- | --- | --- |
| `signUp` | `POST /auth/v1/signup` | works |
| `verifyOtp` | `POST /auth/v1/verify` | works |
| `signInWithPassword` | `POST /auth/v1/token?grant_type=password` | works |
| `getUser` | `GET /auth/v1/user` | works |
| `refreshSession` | `POST /auth/v1/token?grant_type=refresh_token` | works |
| `updateUser` | `PUT /auth/v1/user` | password only (LIM-10) |
| `signInWithOtp` | `POST /auth/v1/otp` | works: magic link and 6-digit code |
| `resetPasswordForEmail` | `POST /auth/v1/recover` | works |
| `signOut` | `POST /auth/v1/logout` | works, scopes `global`, `local` and `others` |

Also served: `signInWithOAuth` to twelve providers, in implicit and PKCE flows, and the two-factor authentication of `supabase.auth.mfa` (TOTP and WebAuthn key), with the `aal` and `amr` claims in the token. A policy can require the second factor: `(select auth.jwt() ->> 'aal') = 'aal2'`.

Not served:

- enterprise SAML sign-in (`signInWithSSO`), identity linking (`linkIdentity`) and `/auth/v1/settings`: 404, code `chemin_auth_inconnu` (LIM-06). Enterprise sign-in goes through Keycloak, over OpenID Connect;
- changing the address or metadata through `updateUser`: 400, codes `changement_d_adresse_ailleurs` and `metadonnees_non_rangees` (LIM-10). The address is changed with `POST /auth/v1/user/email`, with the password then two confirmations; metadata belongs in a project table;
- anonymous sign-in, anti-bot challenge verification and web3 sign-in. The "anonymous accounts" and "anti-bot challenge" settings are stored, but no flow applies them yet.

## Identity providers

Twelve providers, under their `supabase-js` names: `apple`, `azure` (Microsoft Entra ID), `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `slack_oidc`, `twitch`.

1. At the provider, register the callback address: `https://<identifier>.donnees.bastide.app/auth/v1/callback`.
2. In Bastide, set the client identifier and its secret (for Apple: the `.p8` key, the team identifier and the key identifier; for Keycloak: the instance address, required; for GitLab and Entra ID: the instance or tenant address, if needed).
3. Enable the provider. It takes effect on the call, with no restart. Enabling requires a secret to be set.

The secret is sealed and never read back: the console only says that it is set. An address the provider does not report as verified (never for Entra ID or Twitch) does not join an existing account.

## Site address and redirects

- **The site address** (`SITE_URL`) is your application's. Email links lead there.
- **Allowed redirects** (`URI_ALLOW_LIST`, twenty at most): each one allows the paths beneath it. A redirect requested outside this list falls back to the site address.

Addresses use `https://`, except `http://localhost` and `http://127.0.0.1` for development.

An email link always leads to a page on **your** site, `<site address>/auth/confirm?token=…&type=…`, never straight to the platform: antivirus scanners follow links before people do, and a token is only consumed by a `POST`. Your page reads `token` and `type`, then presents it on an explicit action:

| `type` | Path that consumes the token |
| --- | --- |
| `signup` | `POST /auth/v1/verify` |
| `recovery` | `POST /auth/v1/reset` |
| `magiclink` | `POST /auth/v1/magiclink/verify` |
| `email_change_current` | `POST /auth/v1/user/email/confirm-old` |
| `email_change_new` | `POST /auth/v1/user/email/confirm-new` |

Without a site address, the sign-up confirmation email alone leads to a page served at the project address, whose button posts the token.

## Authentication emails

The platform sends six kinds of email: address confirmation, a notice of sign-up on an already confirmed address, password reset, the two confirmations of an address change, and passwordless sign-in (magic link and 6-digit code in the same message). Templates are the platform's, in French; per-project customisation is not available yet.

The sending log is read in the « Courriels d'authentification » (authentication emails) card of the **Authentification** (Authentication) screen, or with `auth_email_list`: for each email, its kind, its state (`pending`, `delivered`, `rejected`, `dead`), its attempts and the relay's answer, then the bounces and complaints that came back after delivery. Never the address or the link. The log is kept 30 days. Sending rules (retries, hourly cap, suppression list) are in [Emails](emails.md).

## Hourly limits

Each project sets its rate limits. Going over returns 429, code `debit_depasse`, with a `Retry-After` header.

| Setting | Default | Range |
| --- | --- | --- |
| Sign-ups per hour per IP address | 30 | 1 to 300 |
| Emails per hour per address | 4 | 1 to 40 |
| Session refreshes per hour per session | 30 | 1 to 300 |
| Attempts on a token or code | 5 | 1 to 10 |
| Magic link and code validity, in minutes | 15 | 5 to 60 |

## Backend keys

| Key | Form | Where it lives | How many |
| --- | --- | --- | --- |
| Publishable | `sb_publishable_…` | In browser code: it is public by design | One per project |
| Secret | `sb_secret_…` | Server side only | Several, revocable one by one |

- **A secret key is shown once**, when it is created: the database keeps only a salted hash. Issue one per use (server, job, tool) so you can revoke one without breaking the others.
- **A secret key presented by a browser** is refused (401, `cle_secrete_depuis_un_navigateur`), and the posture analysis names it until it is revoked.
- **The publishable key is replaced** in a single step: the old one is revoked and the new one issued in the same transaction, so no read ever finds the project without a key.
- **The legacy keys** `anon` and `service_role` only work for a project imported from Supabase, for twelve months (LIM-09, see [Migrate from Supabase](from-supabase.md)).

Issuing, replacing and revoking require the `admin` rank and an account holding a second factor. A replacement or a revocation can be undone within five minutes; after that, issue a new key.

## From the console

- **Authentification**: the twelve providers on the left; the one you pick opens on the right, with its identifier, its secret (replaced, never read back) and its switch. Below, the « Adresse du site et redirections » (site address and redirects) card and the « Courriels d'authentification » card.
- **Données** (Data), section « Clés et points d'entrée » (keys and endpoints): the key list, « Frapper une clé secrète » (issue a secret key), « Remplacer la clé publiable… » (replace the publishable key) and « Révoquer » (revoke) on each secret key. A dialog names the key and the effect before acting.

## Through an agent, the command line or the API

| Action | MCP tool | Command line |
| --- | --- | --- |
| Read the settings | `backend_auth_read` | `bastide backend auth read` |
| Configure authentication | `backend_auth_configure` | `bastide backend auth configure` |
| Configure a provider | `backend_auth_provider_configure` | `bastide backend auth_provider configure` |
| Read the email log | `auth_email_list` | `bastide auth_email list` |
| Read the keys | `backend_read` | `bastide backend read` |
| Replace the publishable key | `backend_key_replace` | `bastide backend key replace` |
| Revoke a secret key | `backend_key_revoke` | `bastide backend key revoke` |
| Issue a secret key | none | `bastide backend key create` |

Issuing a secret key has no MCP tool: no tool returns the value of a secret. A person issues it from the console, the command line or the API (`POST /v1/projects/{projectId}/backend/keys`).

Set the site address and limits (`PATCH /v1/projects/{projectId}/backend/auth`); an absent field stays as it is:

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "urlDuSite": "https://www.example.com",
  "redirectionsAdmises": ["https://www.example.com/account", "http://localhost:3000"],
  "inscriptionsParHeureParIp": 60,
  "idempotencyKey": "auth-site-2026-10-28"
}
```

The response carries an `undoToken`: `action_undo` reverts the setting within five minutes.

Enable GitHub (`PUT /v1/projects/{projectId}/backend/auth/providers`):

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "fournisseur": "github",
  "identifiantClient": "Iv1.0123456789abcdef",
  "secret": "<OAuth client secret>",
  "actif": true,
  "idempotencyKey": "auth-github-2026-10-28"
}
```

These writes are not open to service accounts. All of them refuse a project without a database.

## Common errors

- **429 `debit_depasse`**: an hourly limit was reached; wait for `Retry-After`, or raise the limit.
- **401 `cle_refusee` or `cle_absente`**: the key is missing, revoked, or not this project's.
- **401 `cle_secrete_depuis_un_navigateur`**: use the publishable key in the browser, and revoke the exposed secret key.
- **404 `chemin_auth_inconnu`**: the call is not served (SAML, `linkIdentity`, `/settings`).
- **The email link lands on the home page**: the requested redirect is not among the allowed redirects.
- **No email arrives**: read the sending log; the address may have been suspended after a bounce (see [Emails](emails.md)).

## See also

- [Data security](data-security.md), [Emails](emails.md), [Database](database.md), [Migrate from Supabase](from-supabase.md)
- Reference: [backend.auth.configure](../reference/operations/backend.auth.configure.md), [backend.auth_provider.configure](../reference/operations/backend.auth_provider.configure.md), [auth_email.list](../reference/operations/auth_email.list.md), [backend.key.create](../reference/operations/backend.key.create.md)
