# Auth Every Unbase project ships a built-in, Supabase-style **Auth service** for authenticating the **end users of your app** — the people who sign into the product you build on Unbase, not you (the project owner). It hands those users JWT sessions you can verify in your own backend. All endpoints live under: ``` AUTH = https://api.unbase.dev/v1/projects/:id/auth ``` ## Keys and headers Your project has two keys (both returned by `POST /v1/projects`): - **Anon key** — `${projectId}.pk.${signature}`. Safe to embed in a client app. Sent in an `apikey` header, it unlocks the Auth endpoints only — it cannot read or write your SQL tables. - **Secret key** — `${projectId}.${signature}`. Full data-plane + admin access. Also accepted in the `apikey` header, and required for the two admin endpoints. **Never expose it in a browser.** Every Auth call authenticates with the **anon key** in an `apikey` header (the secret key works too): ``` apikey: unbase_abc123.pk.f0e1d2... ``` Endpoints that act on a signed-in user (`/password`, `/user`) additionally take that user's access token as a bearer token: ``` apikey: unbase_abc123.pk.f0e1d2... authorization: Bearer ``` ## The Session object `signup`, `signin`, `verify`, `token`, `password`, and `reset` all return a **Session**: ```json { "accessToken": "", "tokenType": "bearer", "expiresIn": 3600, "expiresAt": 1751824800, "refreshToken": "", "user": { "id": "user_...", "email": "u@x.com", "emailConfirmedAt": null, "createdAt": 1751821200000, "lastSignInAt": 1751821200000 } } ``` The `accessToken` is an **HS256 JWT signed with the project's own JWT secret** (retrievable via `GET {AUTH}/settings`), so you can verify end-user tokens in your own backend with any standard JWT library. Claims: ```json { "sub": "user_...", "iss": "", "role": "authenticated", "email": "u@x.com", "iat": 1751821200, "exp": 1751824800 } ``` - `accessToken` is short-lived (1 hour). Refresh it with `POST {AUTH}/token`. - `refreshToken` is long-lived (30 days) and **single-use** — each exchange rotates it, so always store the new one. ## Endpoints | Method & path | Auth | Body | Returns | |---|---|---|---| | `POST {AUTH}/signup` | `apikey` | `{ email, password }` | `201`, Session | | `POST {AUTH}/signin` | `apikey` | `{ email, password }` | `200`, Session | | `POST {AUTH}/magiclink` | `apikey` | `{ email, redirectTo? }` | `{ sent, devLink? }` | | `POST {AUTH}/verify` | `apikey` | `{ token }` | `200`, Session | | `POST {AUTH}/token` | `apikey` | `{ refreshToken }` | `200`, Session | | `POST {AUTH}/password` | `apikey` + bearer | `{ currentPassword?, newPassword }` | `200`, Session | | `POST {AUTH}/recover` | `apikey` | `{ email, redirectTo? }` | `{ sent, devLink? }` | | `POST {AUTH}/reset` | `apikey` | `{ token, newPassword }` | `200`, Session | | `POST {AUTH}/user` | `apikey` + bearer | `{}` | `{ user }` | | `POST {AUTH}/logout` | `apikey` | `{ refreshToken }` | `204` | | `GET {AUTH}/users` | secret key (admin) | — | `{ users: [...] }` | | `GET {AUTH}/settings` | secret key (admin) | — | `{ jwtSecret, userCount, anonKey, authUrl }` | Passwords must be at least **8 characters**. Email addresses are normalized (trimmed and lower-cased). ## Email + password Sign a user up and receive a Session in one call: ```bash curl -s -X POST "$AUTH/signup" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"email":"user@example.com","password":"hunter2!!"}' ``` `signin` is identical but returns `200` (and `401` on a wrong password). A duplicate `signup` returns `409`. ## Passwordless (magic link) 1. Request a link. Unbase emails the user a URL pointing at `redirectTo?token=...` (falling back to `/auth/callback?token=...`). In keyless dev mode nothing is emailed and the link comes back inline as `devLink`. ```bash curl -s -X POST "$AUTH/magiclink" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"email":"user@example.com","redirectTo":"https://myapp.example/welcome"}' ``` 2. Your app pulls the `token` off the redirect URL and exchanges it for a Session. The user is created on first verify. ```bash curl -s -X POST "$AUTH/verify" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"token":""}' ``` ## Refreshing and signing out Exchange a refresh token for a fresh Session (the old refresh token is immediately invalidated): ```bash curl -s -X POST "$AUTH/token" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"refreshToken":""}' ``` Revoke a refresh token to sign the user out: ```bash curl -s -X POST "$AUTH/logout" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"refreshToken":""}' ``` ## Changing a password (signed in) A signed-in user changes their own password by presenting their **access token** as a bearer token alongside the `apikey`: ```bash curl -s -X POST "$AUTH/password" \ -H "apikey: $ANON_KEY" \ -H "authorization: Bearer $ACCESS_TOKEN" \ -H "content-type: application/json" \ -d '{"currentPassword":"hunter2!!","newPassword":"a-stronger-passphrase"}' ``` - A user who already has a password **must** supply the correct `currentPassword` (`401` if it's wrong). - A passwordless (magic-link-only) user **omits** `currentPassword` to set a password for the first time. - The new password must be at least 8 characters. - Changing the password **revokes the user's other sessions** and returns a fresh Session, so the caller stays signed in while any other devices are logged out. ## Resetting a forgotten password A two-step flow, mirroring magic link. 1. **Request a reset link.** Unbase mints a stateless, **1-hour** recovery token and emails a link pointing at `redirectTo?token=...` (falling back to `/auth/reset?token=...`). This endpoint **always reports success**, never revealing whether an account exists. Keyless dev mode returns the link inline as `devLink`. ```bash curl -s -X POST "$AUTH/recover" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"email":"user@example.com","redirectTo":"https://myapp.example/reset"}' ``` 2. **Set the new password.** Your app pulls the `token` off the redirect URL and posts it with the new password. This sets the password, **revokes all prior sessions**, and returns a fresh Session so the user is signed straight in. ```bash curl -s -X POST "$AUTH/reset" \ -H "apikey: $ANON_KEY" -H "content-type: application/json" \ -d '{"token":"","newPassword":"a-brand-new-passphrase"}' ``` ## The current user Resolve an access token to its user (verifies the JWT signature and expiry): ```bash curl -s -X POST "$AUTH/user" \ -H "apikey: $ANON_KEY" \ -H "authorization: Bearer $ACCESS_TOKEN" \ -H "content-type: application/json" -d '{}' ``` Returns `{ "user": { ... } }`, or `401` if the token is missing, invalid, or expired. ## Admin endpoints (secret key only) These require the **secret key** as `Authorization: Bearer ` — the anon key is rejected. - `GET {AUTH}/users` → `{ users: [...] }` — every end user registered with the project (most recent first). - `GET {AUTH}/settings` → `{ jwtSecret, userCount, anonKey, authUrl }` — the project's Auth configuration, including the JWT secret you need to verify end-user access tokens in your own backend. ## Errors All errors are JSON: `{ "error": { "code": "...", "message": "..." } }`. | HTTP | code | When | |---|---|---| | 400 | `bad_request` | Missing/short password, missing or malformed/expired token, invalid email | | 401 | `unauthorized` | Missing/invalid `apikey`, wrong `currentPassword`, invalid or expired access/refresh token, token for a different project | | 409 | `conflict` | `signup` where the email already exists | ## See also - [`openapi.yaml`](openapi.yaml) — full machine-readable schemas for every Auth endpoint (`tags: [auth]`). - [`llms.txt`](llms.txt) — dense, AI-agent-friendly API reference. - [`quickstart.md`](quickstart.md) — five-minute intro covering project creation and running SQL.