> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lala.ist/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Every /api route needs a Supabase access token in the Authorization header.

Lala does not have its own login, and it does not issue API keys. The client signs in against
Lala's Supabase project. The client then sends the Supabase access token to this API.

## The header

Send the access token as a bearer token:

```http theme={null}
Authorization: Bearer <supabase_access_token>
```

Every route under `/api` needs this header. Only `GET /health` works without it.

## How the server verifies the token

The server verifies the token with `jose`, against the JWKS of the Lala Supabase project:

```text theme={null}
GET <SUPABASE_URL>/auth/v1/.well-known/jwks.json
```

The key set is cached in the process. The server also verifies that the `aud` claim is
`authenticated`. A token that is signed with the legacy HS256 shared secret still verifies while
`SUPABASE_JWT_SECRET` is set on the server. Asymmetric keys are the default, so a normal client
does not need to know this.

The `sub` claim is the identity. The server uses it as the student id, so `id` in the `/api/me`
response is the Supabase `auth.users.id`.

<Note>
  Nothing keys on a phone number or an email address. The provider can change without a schema
  change.
</Note>

## Get a token in the client

The token comes from the Supabase client library, not from this API.

<CodeGroup>
  ```dart Dart theme={null}
  final supabase = Supabase.instance.client;
  await supabase.auth.signInWithOtp(email: 'ogrenci@example.com');
  // After the student enters the code:
  final session = supabase.auth.currentSession;
  final token = session?.accessToken;
  ```

  ```javascript JavaScript theme={null}
  const { data } = await supabase.auth.getSession();
  const token = data.session?.access_token;
  ```
</CodeGroup>

## Token expiry

A Supabase access token is short-lived. The Supabase client library refreshes it in the
background. Read the token from the session immediately before each request. Do not cache the
string for the lifetime of the application.

If the API returns `401`, refresh the session once and send the request again. If the second
attempt also returns `401`, sign the student out.

## The 401 response

A missing header, a malformed header, an expired token, or a bad signature all produce the same
response. The server does not say which one failed.

```http theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{ "error": "unauthorized" }
```

<Warning>
  `POST /api/chat/send` returns `401` only before the stream starts. After the stream starts, the
  status is `200` and any failure arrives as an SSE `error` event. Read
  [Errors and limits](/guides/errors-and-limits).
</Warning>

## First contact creates the student

The student row is created on the first `GET /api/me`. The other routes load the student too, so
they also create the row. Call `GET /api/me` right after sign-in anyway. The response tells you
whether to show the onboarding screen.
