API Key Management
Understanding Supabase API Keys
Supabase provides several types of API keys, each with different purposes and security implications. Understanding when to use each is critical for building secure applications.
The Three Key Types
1. anon (Public) Key
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Purpose: Client-side requests where no user is logged in
Characteristics:
- Safe to include in frontend code
- Included in every request from the client
- Has the PostgreSQL role
anon - Subject to all RLS policies
Use for:
- Initializing the Supabase client in browsers
- Public API access
- Unauthenticated operations
// Frontend: anon key is expected
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key' // This is public
)
2. service_role (Secret) Key
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Purpose: Server-side admin operations
Characteristics:
- MUST be kept secret
- Bypasses ALL Row Level Security
- Full database access
- Has the PostgreSQL role
service_role
Use for:
- Server-side operations (Edge Functions, backend APIs)
- Admin tasks
- Operations that need to bypass RLS
// Server-side only (Edge Function, backend)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // NEVER expose!
)
// This bypasses RLS - be careful!
const { data } = await supabase.from('users').select('*')
3. JWT Secret
your-jwt-secret-at-least-32-characters
Purpose: Signing and verifying JWT tokens
Characteristics:
- Used internally by GoTrue
- Can verify JWTs in your own backend
- Never expose to clients
Key Security Rules
Rule 1: anon Key CAN Be Public
// This is fine - anon key is designed for this
const supabase = createClient(url, anonKey)
The anon key identifies your project but doesn't grant special permissions. RLS policies protect your data.
Rule 2: service_role Key MUST Be Secret
// NEVER do this
const supabase = createClient(url, 'eyJ..service_role_key...')
// NEVER put in environment variables that reach the browser
NEXT_PUBLIC_SUPABASE_SERVICE_KEY=xxx // WRONG!
// NEVER commit to version control
If the service_role key is exposed:
- Attacker can read ALL data (bypasses RLS)
- Attacker can modify ALL data
- Attacker can delete ALL data
- Game over
Rule 3: Rotate Compromised Keys
If you suspect key compromise:
- Go to Supabase Dashboard → Settings → API
- Generate new keys
- Update your applications
- The old keys become invalid
Where to Store Keys
Frontend (Browser)
// anon key only - public anyway
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
Backend (Server)
// service_role key - keep secret
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
)
Edge Functions
// Automatically available as env vars
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Environment Variable Naming
Follow platform conventions for public vs private:
Next.js
# Public (sent to browser) - NEXT_PUBLIC_ prefix
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
# Private (server only) - no prefix
SUPABASE_SERVICE_ROLE_KEY=eyJ...
Vite
# Public - VITE_ prefix
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
# Private - no prefix (won't be bundled)
SUPABASE_SERVICE_ROLE_KEY=eyJ...
Node.js Backend
# All private on server
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
When to Use Each Key
Use anon Key When:
- Building frontend/client applications
- User is not logged in (or is logged in with their own JWT)
- You want RLS to apply
- Making requests from browser/mobile apps
// Client: User's requests go through RLS
const { data } = await supabase
.from('posts')
.select('*')
// RLS filters to what this user can see
Use service_role Key When:
- Running in a secure server environment
- Need to bypass RLS intentionally
- Performing admin operations
- Background jobs or cron tasks
- Webhook handlers that need full access
// Server: Admin operation
const { data } = await adminSupabase
.from('users')
.select('*')
// Returns ALL users (no RLS filtering)
Mixing Keys in Applications
A typical full-stack app uses both:
// pages/api/admin/users.ts (Next.js API route)
import { createClient } from '@supabase/supabase-js'
// Admin client for elevated operations
const adminClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Server-side only
)
// User client from request
function getUserClient(req) {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
global: {
headers: { Authorization: req.headers.authorization }
}
}
)
}
export default async function handler(req, res) {
// Verify user is admin using their JWT
const userClient = getUserClient(req)
const { data: { user } } = await userClient.auth.getUser()
if (!user) {
return res.status(401).json({ error: 'Unauthorized' })
}
// Check admin status
const { data: profile } = await userClient
.from('profiles')
.select('role')
.eq('id', user.id)
.single()
if (profile?.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' })
}
// Now safe to use admin client
const { data: users } = await adminClient
.from('profiles')
.select('*')
res.json(users)
}
Key Takeaways
- anon key is public: Designed to be in client code
- service_role key is secret: Never expose, bypasses RLS
- Use environment variables: Platform-appropriate naming
- Rotate if compromised: Generate new keys immediately
- Match key to context: Frontend = anon, Server = service_role
- Verify before elevating: Check permissions before using admin key
Next Steps
Understanding keys is essential. Next, we'll explore common security mistakes and how to avoid them.
API keys are like physical keys. The anon key is like a lobby key—anyone can use it, but it only opens public doors. The service_role key is like a master key—it opens everything, so guard it accordingly.

