# Admin API (auth.admin)
Source: https://sovaria.in/docs/auth-admin-api

Create, list, update, ban, invite and delete app users from your server code and generate sign-in, recovery and invite links: supabase's auth.admin in every SDK, with who may call it, limits and errors.

`auth.admin` manages your **app users** from code you run yourself — a back office, a support tool, a migration script, a cron job. It is supabase's admin API with the same names. It is the same work the console's **Users** page does, under the same checks.

> **Server code only:** Admin calls need an **org owner or admin** session (a team member signed in with `auth.consoleLogin`, or a server key used from your backend). A publishable/anon key or an app user's session is refused (`401`/`403`). Never ship admin code in a browser or mobile app. For an admin screen **inside** your app, use [auth.users](/docs/app-user-admins).

TypeScript:
```ts
import { createClient } from '@sovaria/sdk'

const admin = createClient(SOVARIA_URL, SERVER_KEY, { results: 'supabase' })   // or a team member's session

const { data: { user }, error } = await admin.auth.admin.createUser({
  email: 'ada@example.com', password: 'Correct-Horse-9!', email_confirm: true,
  user_metadata: { name: 'Ada' }, app_metadata: { plan: 'pro' }, roles: ['librarian'],
})
const { data: page } = await admin.auth.admin.listUsers({ page: 1, perPage: 100 })     // { users, nextPage, lastPage, total }
const { data: one } = await admin.auth.admin.getUserById(user.id)
await admin.auth.admin.updateUserById(user.id, { app_metadata: { plan: 'team' }, ban_duration: '24h' })
await admin.auth.admin.inviteUserByEmail('grace@example.com', { data: { team: 'blue' } })
// A temporary password made by the server, returned once; the user must change it at first sign-in:
const { data: first } = await admin.auth.admin.createUser({ email: 'lin@example.com', roles: ['STAFF'], temporary_password: true })
// first.temporaryPassword → hand it to Lin (never log or store it)
const { data: link } = await admin.auth.admin.generateLink({ type: 'recovery', email: 'ada@example.com' })
const { data: signup } = await admin.auth.admin.generateLink({ type: 'signup', email: 'lin@example.com', password: 'Correct-Horse-9!' })
await admin.auth.admin.deleteUser(user.id)
```

Python:
```python
user = await sov.auth.admin.create_user({"email": "ada@example.com", "password": "Correct-Horse-9!",
                                        "email_confirm": True, "user_metadata": {"name": "Ada"}})
first = await sov.auth.admin.create_user({"email": "lin@example.com", "roles": ["STAFF"], "temporary_password": True})
# first["temporary_password"]: shown once; Lin changes it at first sign-in
users = await sov.auth.admin.list_users(page=1, per_page=100)        # users.total, users.next_page
await sov.auth.admin.update_user_by_id(user["id"], {"app_metadata": {"plan": "team"}, "ban_duration": "none"})
link = await sov.auth.admin.generate_link({"type": "magiclink", "email": "ada@example.com"})
await sov.auth.admin.delete_user(user["id"])
```

Swift:
```swift
let user = try await sovaria.auth.admin.createUser(email: "ada@example.com", password: "Correct-Horse-9!",
                                                   emailConfirm: true, userMetadata: ["name": "Ada"])
let page = try await sovaria.auth.admin.listUsers(page: 1, perPage: 100)
try await sovaria.auth.admin.updateUserById(user["id"] as! String, banDuration: "24h")
let link = try await sovaria.auth.admin.generateLink(type: "recovery", email: "ada@example.com")
```

Kotlin:
```kotlin
val user = sovaria.auth.admin.createUser("ada@example.com", password = "Correct-Horse-9!",
    emailConfirm = true, userMetadata = mapOf("name" to "Ada"))
val page = sovaria.auth.admin.listUsers(page = 1, perPage = 100)
sovaria.auth.admin.updateUserById(user["id"].toString(), banDuration = "none")
val link = sovaria.auth.admin.generateLink("invite", "grace@example.com", data = mapOf("team" to "blue"))
```

Dart:
```dart
final user = await sovaria.auth.admin.createUser(email: 'ada@example.com', password: 'Correct-Horse-9!',
    emailConfirm: true, userMetadata: {'name': 'Ada'});
final page = await sovaria.auth.admin.listUsers(page: 1, perPage: 100);
await sovaria.auth.admin.updateUserById('${user['id']}', appMetadata: {'plan': 'team'});
final link = await sovaria.auth.admin.generateLink(type: 'magiclink', email: 'ada@example.com');
```

## Every call

| Call | What it does | HTTP |
|---|---|---|
| `createUser({ email, password?, email_confirm?, user_metadata?, app_metadata?, roles? })` | Creates the user. Without a password they sign in by link or code. `email_confirm` skips the confirmation email. | `POST /projects/{id}/auth/users`, then `PATCH …/users/{uid}` for metadata |
| `listUsers({ page, perPage })` | Newest first, up to 500 a page and 50 pages deep (use `auth.productUsers.listPage` and its cursor for more). `total` comes with the answer. | `GET /projects/{id}/auth/users` |
| `getUserById(id)` | The Users row plus `user_metadata`, `app_metadata`, custom claims and `banned`. | `GET /projects/{id}/auth/users/{uid}` |
| `updateUserById(id, { user_metadata?, app_metadata?, email_confirm?, roles?, ban_duration? })` | Metadata is merged (a `null` value removes the key); `app_metadata` only admins can set; `ban_duration: 'none'` lifts a ban, any other value bans until you lift it. | `PATCH …/users/{uid}`, `PUT …/roles`, `POST …/ban` |
| `deleteUser(id)` | Deletes the user and their sessions. | `DELETE /projects/{id}/auth/users/{uid}` |
| `inviteUserByEmail(email, { data? })` | Creates a passwordless user and sends your project's sign-in email. | `POST /projects/{id}/auth/users` (`send_magic_link`) |
| `generateLink({ type, email, password?, options })` | Returns the link an email would carry — `magiclink`, `recovery`, `invite` (creates a passwordless user) or `signup` (creates a user with `password`, address not yet confirmed; the link confirms it) — with the 6-digit `email_otp` (sign-in links) and the `hashed_token` for `verifyOtp({ token_hash, type })`. `created: true` when the call created the user. Nothing is emailed. CLI: `sov auth-product link --type signup --email …`. | `POST /projects/{id}/auth/generate-link` |
| `mfa.listFactors({ userId })` / `mfa.deleteFactors({ userId })` | A user's second factors; remove them all (they enroll again; backup codes are revoked). | `GET` / `POST /projects/{id}/auth/users/{uid}/mfa[/reset]` |

## Security and limits

- Every call checks that the caller administers **this** project's users; a user of another project is a `404`.
- `generateLink` is for org owners and admins, with a team member's session (API keys are refused): at most **60 links an hour** per admin, and each one is written to the audit log (`auth.admin.link_generated`, with the type and the user). The link signs that user in — send it only to them, never log it.
- Invite links last 24 hours; magic links, recovery links and signup confirmation links last as long as the emailed ones (Auth › Settings). A `signup` password must pass the project's password policy, and the audit entry records `created: true` (never the password).
- User changes are recorded in the project history by field name (values can be personal data, so they are not).
- Email, password and phone belong to the user: they change them with `updateUser`, or you send a recovery link.

## Errors

| Status | Meaning |
|---|---|
| `401` | No session, or an expired one. |
| `403` | Not an owner/admin of the organization, an app user's session, or an API key where a person is needed (`generateLink`). |
| `404` | No such user in this project (or, for `magiclink`/`recovery`, no active user with that email). |
| `409` | `EMAIL_EXISTS`: the address already has an account in this project (`createUser`, `invite`, `signup`). |
| `422` | A reserved claim, metadata that is too large, a `signup` link without a password (`PASSWORD_REQUIRED`) or one the password policy refuses. |
| `429` | `ADMIN_LINK_RATE_LIMITED`: more than 60 links in the last hour. |
