How to fix not_admin: User is not an admin in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 15, 2025· 191 views

Supabase responds with "not_admin: User is not an admin" when you attempt to call an admin-only operation using a regular user JWT instead of a service_role key. Admin operations like createUser, updateUserById, and deleteUser require elevated privileges that bypass Row Level Security and can only be executed on trusted servers with the service_role key.

Quick fix

Fixes not_admin

// lib/supabase-admin.ts (server-only file)
import { createClient } from '@supabase/supabase-js'

// Never expose this in client code
export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // NOT the anon key
  {
    auth: {
      autoRefreshToken: false,
      persistSession: false
    }
  }
)
  1. Create a server-side admin client with the service_role key
  2. Move admin operations to API routes or server functions
  3. Verify you're not exposing the service_role key to the client
  4. Implement authorization checks before admin operations
supabase — error
// lib/supabase-admin.ts (server-only file)
import { createClient } from '@supabase/supabase-js'
// Never expose this in client code
export const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // NOT the anon key
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)
not_admin: User is not an admin

What this error means

The Supabase Auth Admin API provides privileged operations that allow creating, updating, and deleting users without normal authentication constraints. These operations live under the `supabase.auth.admin` namespace and require a JWT with the `service_role` or `supabase_admin` role claim. When you call these methods with a regular user token (authenticated via the anon key), Supabase checks the JWT's role claim and immediately rejects the request with "not_admin: User is not an admin" because: - Regular user JWTs have the `authenticated` role, not `service_role` or `supabase_admin` - Admin operations bypass Row Level Security (RLS), which would be a security risk if available to regular users - The service_role key creates a client with superuser privileges meant only for trusted server environments The fix always involves moving admin operations to a secure server-side context and using the service_role key instead of the public anon key.

Typical symptoms

  • Calls to `supabase.auth.admin.createUser()`, `updateUserById()`, or `deleteUser()` fail with HTTP 403 and the message "not_admin: User is not an admin".
  • The error occurs in client-side code (browser, mobile app) where the anon/public key is used to initialize the Supabase client.
  • Server logs show the JWT role claim is 'authenticated' or 'anon' instead of 'service_role' when the admin method is invoked.
  • Attempting to update auth.users directly via Postgres queries results in permission denied errors because the supabase_auth_admin role owns that schema.
  • Edge Functions or API routes fail with this error when using `createClient()` with the anon key instead of the service_role key.

Common causes

  1. Using the anon/public key (NEXT_PUBLIC_SUPABASE_ANON_KEY) to initialize the Supabase client on the server when calling admin methods.
  2. Calling supabase.auth.admin methods from client-side JavaScript code where only the anon key is available.
  3. Forgetting to create a separate server-side Supabase client with the service_role key for administrative operations.
  4. Passing a user's session token to an admin endpoint that expects a service_role JWT.
  5. Exposing the service_role key in environment variables prefixed with NEXT_PUBLIC_ or similar client-accessible locations.
  6. Using the wrong environment variable name when initializing the admin client (e.g., SUPABASE_KEY instead of SUPABASE_SERVICE_ROLE_KEY).

How to fix "not_admin: User is not an admin"

1Create a server-side admin client with the service_role key

Admin operations must run on a trusted server (Next.js API routes, Edge Functions, backend services) using the service_role key:

typescript
// lib/supabase-admin.ts (server-only file)
import { createClient } from '@supabase/supabase-js'

// Never expose this in client code
export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // NOT the anon key
  {
    auth: {
      autoRefreshToken: false,
      persistSession: false
    }
  }
)

Ensure your environment variables are set correctly:

bash
# .env.local (server-side only)
SUPABASE_URL=https://yourproject.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...  # From Project Settings → API → service_role

The service_role key bypasses RLS and grants full database access, so keep it secret and server-side only.

2Move admin operations to API routes or server functions

Admin methods cannot run in client-side code. Move them to Next.js API routes, server actions, or Edge Functions:

typescript
// app/api/admin/create-user/route.ts
import { supabaseAdmin } from '@/lib/supabase-admin'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  // Verify the request is authorized (implement your own auth check)
  const session = await getServerSession()
  if (!session?.user?.isAdmin) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
  }

  const { email, password } = await request.json()

  const { data, error } = await supabaseAdmin.auth.admin.createUser({
    email,
    password,
    email_confirm: true
  })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 })
  }

  return NextResponse.json({ user: data.user })
}

Client-side code then calls your API endpoint instead of using admin methods directly:

typescript
// Client-side component
const response = await fetch('/api/admin/create-user', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password })
})
3Verify you're not exposing the service_role key to the client

Environment variables prefixed with NEXT_PUBLIC_, VITE_, or REACT_APP_ are bundled into client code and publicly accessible. Never use these prefixes for the service_role key:

❌ Wrong (exposes to client):

bash
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...

✅ Correct (server-only):

bash
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...

In your code, verify the client is initialized correctly:

typescript
// For regular users (client-side)
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// For admin operations (server-side ONLY)
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // No NEXT_PUBLIC prefix
)

Check your browser's Network tab or inspect bundled JavaScript to ensure the service_role key never appears.

4Implement authorization checks before admin operations

Even when using the service_role key on the server, add your own authorization logic to prevent unauthorized users from triggering admin operations:

typescript
import { supabaseAdmin } from '@/lib/supabase-admin'
import { createClient } from '@supabase/supabase-js'

export async function POST(request: NextRequest) {
  // Create a regular client to verify the user's session
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Check custom claims or a roles table
  const { data: profile } = await supabase
    .from('profiles')
    .select('role')
    .eq('id', user.id)
    .single()

  if (profile?.role !== 'admin') {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
  }

  // Now safe to use admin operations
  const { data, error } = await supabaseAdmin.auth.admin.updateUserById(
    targetUserId,
    { email: newEmail }
  )

  return NextResponse.json({ data, error })
}

This prevents regular users from calling your admin endpoints even if they discover the API route.

Advanced notes

### JWT role claims and the service_role key

When you initialize a Supabase client with the service_role key, the JWT issued by that client includes a role claim set to service_role. This role bypasses all Row Level Security policies and grants unrestricted access to your database. Regular user JWTs have the authenticated role, which respects RLS policies. The not_admin error specifically checks for the presence of service_role or supabase_admin in the JWT's role claim before allowing admin operations.

### Difference between anon key and service_role key

- Anon key: Safe to expose in client code, respects RLS, only allows operations permitted by your security policies
- Service_role key: Must remain secret, bypasses RLS, grants full database access including auth.users modifications

You can find both keys in your Supabase dashboard under Project Settings → API. Never commit the service_role key to version control or expose it in client bundles.

### Edge Functions and service_role access

Supabase Edge Functions can access the service_role key via environment variables without exposing it to clients:

typescript
// supabase/functions/admin-operation/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const supabaseAdmin = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  const { data, error } = await supabaseAdmin.auth.admin.listUsers()

  return new Response(JSON.stringify({ data, error }), {
    headers: { 'Content-Type': 'application/json' }
  })
})

Deploy the function with supabase functions deploy and call it from client code using the anon key.

### Custom roles and RBAC

For more granular control, implement custom roles using Postgres RLS policies and custom JWT claims via Auth Hooks. This lets you grant specific admin privileges to certain users without exposing the full service_role key. See the Supabase documentation on Custom Claims & Role-based Access Control for implementation details.

Frequently asked questions

What does "not_admin" mean?
The Supabase Auth Admin API provides privileged operations that allow creating, updating, and deleting users without normal authentication constraints. admin namespace and require a JWT with the service_role or supabase_admin role claim. When you call these methods with a regular user token (authenticated via the anon key), Supabase checks the JWT's role claim and immediately rejects the request with "not_admin: User is not an admin" because: - Regular user JWTs have the authenticated role, not service_role or supabase_admin - Admin operations bypass Row Level Security (RLS), which would be a security risk if available to regular users - The service_role key creates a client with superuser privileges meant only for trusted server environments The fix always involves moving admin operations to a secure server-side context and using the service_role key instead of the public anon key.
How do I fix "not_admin"?
Supabase responds with "not_admin: User is not an admin" when you attempt to call an admin-only operation using a regular user JWT instead of a service_role key. Admin operations like createUser, updateUserById, and deleteUser require elevated privileges that bypass Row Level Security and can only be executed on trusted servers with the service_role key.
What causes "not_admin"?
Using the anon/public key (NEXT_PUBLIC_SUPABASE_ANON_KEY) to initialize the Supabase client on the server when calling admin methods. Calling supabase.auth.admin methods from client-side JavaScript code where only the anon key is available. Forgetting to create a separate server-side Supabase client with the service_role key for administrative operations.
Is the "not_admin" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading