# Managing users (admin API)

> Create, invite, list, update, ban and delete users in a SupaHost project with the supabase.auth.admin API and the service_role key, safely from a server or an Edge Function.

Source: https://supahost.app/docs/auth/user-management · Updated 2026-11-11

Some user operations shouldn't be available to users themselves: creating an account for someone else, inviting staff, banning an abusive account, setting `app_metadata` that your policies trust. These go through Auth's admin API, which supabase-js exposes as `supabase.auth.admin`.

The admin API needs the project's `service_role` key. That key bypasses Row Level Security and can do anything to any user, so it only ever belongs on a server: an Edge Function, your backend, a script on your laptop. Never ship it to a browser or mobile app.

## Where to get the key

*Dashboard → your project → **Connect** tab → **App*** shows the `service_role` key (labelled "secret") behind a **Reveal** button. Only the instance owner can reveal it. Store it as a server-side environment variable, such as `SUPABASE_SERVICE_ROLE_KEY`.

Edge Functions on SupaHost receive it automatically as `SUPABASE_SERVICE_ROLE_KEY`, so you don't need to add it as a secret there.

## An admin client

```ts
import { createClient } from '@supabase/supabase-js'

export const admin = createClient(
  process.env.SUPABASE_URL!,               // https://app-one.acme.supahost.space
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  { auth: { autoRefreshToken: false, persistSession: false } }
)
```

The two `auth` options stop the client from trying to keep a user session of its own, which an admin client doesn't need.

## Common operations

**Create a user** with a known password, already confirmed:

```ts
const { data, error } = await admin.auth.admin.createUser({
  email: 'priya@example.com',
  password: 'temporary-passphrase-1',
  email_confirm: true,
  user_metadata: { display_name: 'Priya' },
  app_metadata: { team_id: 't_42', role: 'staff' },
})
```

**Invite a user** by email. Auth sends the **Invite** template; the user follows the link, lands on `redirectTo` signed in, and sets a password there with `updateUser({ password })`:

```ts
await admin.auth.admin.inviteUserByEmail('lee@example.com', {
  redirectTo: 'https://rota.example.com/welcome',
  data: { invited_by: 'priya' },
})
```

Invites still work when "Disable new sign-ups" is on, which makes them the usual way to run an invite-only app.

**List users**, a page at a time:

```ts
const { data, error } = await admin.auth.admin.listUsers({ page: 1, perPage: 100 })
for (const user of data.users) console.log(user.id, user.email, user.last_sign_in_at)
```

**Get one user**:

```ts
const { data: { user } } = await admin.auth.admin.getUserById(userId)
```

**Update a user**: change their email or password, confirm them, or set metadata:

```ts
await admin.auth.admin.updateUserById(userId, {
  app_metadata: { role: 'manager' },
})
```

`app_metadata` is merged into what's there, and it appears in the user's JWT from their next token refresh. See [Roles, custom claims and RBAC](/docs/auth/custom-claims-rbac).

**Ban and unban**:

```ts
await admin.auth.admin.updateUserById(userId, { ban_duration: '720h' }) // 30 days
await admin.auth.admin.updateUserById(userId, { ban_duration: 'none' }) // lift the ban
```

A banned user can't sign in or refresh their session. An access token they already hold keeps working until it expires, so combine a ban with a modest [access token lifetime](/docs/auth/sessions).

**Delete a user**:

```ts
await admin.auth.admin.deleteUser(userId)
```

Rows that reference `auth.users` with `on delete cascade` are deleted too. Objects they own in Storage aren't removed automatically; clean those up first if you need to.

## An admin endpoint as an Edge Function

A common pattern is a small Edge Function that your admin screen calls. It checks that the caller is an admin, then uses the admin client. Deploy it from *Dashboard → your project → **Functions** tab*:

```ts
// functions/ban-user/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2'

const admin = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
  { auth: { autoRefreshToken: false, persistSession: false } }
)

Deno.serve(async (req) => {
  const token = req.headers.get('Authorization')?.replace('Bearer ', '') ?? ''
  const { data: { user } } = await admin.auth.getUser(token)

  if (user?.app_metadata?.role !== 'admin') {
    return Response.json({ error: 'forbidden' }, { status: 403 })
  }

  const { userId } = await req.json()
  const { error } = await admin.auth.admin.updateUserById(userId, { ban_duration: '720h' })
  if (error) return Response.json({ error: error.message }, { status: 400 })

  return Response.json({ ok: true })
})
```

The check uses `app_metadata`, which only the `service_role` key can set. Checking `user_metadata` here would let any user make themselves an admin. Call the function from your app with `supabase.functions.invoke('ban-user', { body: { userId } })`; supabase-js sends the signed-in user's token. For browser calls you'll also need CORS headers; see [CORS for browser calls](/docs/functions/cors).

## MFA factors

Remove a factor for a user who has lost their authenticator, after you've confirmed who they are:

```ts
const { data } = await admin.auth.admin.mfa.listFactors({ userId })
await admin.auth.admin.mfa.deleteFactor({ userId, id: data!.factors[0].id })
```

See [Multi-factor authentication](/docs/auth/mfa).

## Generating links without sending email

`admin.auth.admin.generateLink` returns sign-up, invite, magic link and recovery links (plus the one-time code) without sending anything, for when you send emails through your own provider. See [Auth emails and templates](/docs/auth/email-templates).

## Doing it in Studio instead

For one-off tasks, Studio's Authentication page lists users and lets you create, invite and delete them. Open Studio from the project header or *Dashboard → your project → **Database** tab → **Open Studio*** (owner only). See [Using Supabase Studio](/docs/database/studio).

## Related

- [Admin tasks on the server with the service_role key](/kb/server-side-admin-tasks)
- [Errors creating, updating or deleting users](/kb/dashboard-user-errors)
- [Keeping keys safe](/docs/security/api-key-safety)
- The [admin API reference](https://supabase.com/docs/reference/javascript/auth-admin-listusers)
