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.
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
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:
const { data, error } = await admin.auth.admin.createUser({
email: '[email protected]',
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 }):
await admin.auth.admin.inviteUserByEmail('[email protected]', {
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:
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:
const { data: { user } } = await admin.auth.admin.getUserById(userId)
Update a user: change their email or password, confirm them, or set metadata:
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.
Ban and unban:
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.
Delete a user:
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:
// 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.
MFA factors
Remove a factor for a user who has lost their authenticator, after you've confirmed who they are:
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.
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.
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.