The Supabase anon key is designed for client-side applications, but the service role key is the real power user for backend operations.
Let’s see it in action. Imagine we have a simple todos table:
create table todos (
id serial primary key,
title text not null,
is_complete boolean default false,
user_id uuid references auth.users
);
On the client (using the anon key), you’d typically interact with this table like so:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'YOUR_SUPABASE_URL';
const supabaseAnonKey = 'YOUR_ANON_KEY'; // This is the key you'd expose to clients
const supabase = createClient(supabaseUrl, supabaseAnonKey);
async function getMyTodos() {
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', 'some-user-uuid'); // RLS is crucial here
if (error) console.error('Error fetching todos:', error);
return data;
}
async function addTodo(title) {
const { data, error } = await supabase
.from('todos')
.insert([
{ title: title, user_id: 'some-user-uuid' }
]);
if (error) console.error('Error adding todo:', error);
return data;
}
Notice the user_id filter in getMyTodos and the inclusion of user_id in addTodo. This is where Row Level Security (RLS) comes into play. By default, with the anon key, your database operations are restricted by RLS policies. You must define policies that dictate what a user (identified by their auth.uid()) can do. For example, a policy on the todos table might look like this:
-- Enable RLS
alter table todos enable row level security;
-- Policy for reading todos
create policy "Can view own todos"
on todos for select using (auth.uid() = user_id);
-- Policy for inserting todos
create policy "Can insert own todos"
on todos for insert with check (auth.uid() = user_id);
This ensures that a client, even if they somehow got hold of the anon key, can only access or modify their own data. This is a fundamental security principle.
Now, what if you need to perform an operation that bypasses RLS? For instance, an administrative task, a background job, or a server-to-server integration where you don’t have a specific user_id context. This is where the service role key shines.
The service role key is never exposed to the client. It’s meant for your backend services, serverless functions, or administrative scripts. When you use the service role key, RLS is effectively bypassed for that connection.
Let’s say you want to create a new user and immediately assign them a default todo list, or perhaps you need to perform a bulk update on all todos that are overdue.
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'YOUR_SUPABASE_URL';
const supabaseServiceRoleKey = 'YOUR_SERVICE_ROLE_KEY'; // This is the key for backend use
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceRoleKey);
async function createAdminTodo(title) {
// No user_id needed, and RLS is bypassed
const { data, error } = await supabaseAdmin
.from('todos')
.insert([
{ title: title, user_id: null } // Or a specific admin user_id if you have one
]);
if (error) console.error('Error creating admin todo:', error);
return data;
}
async function markAllTodosAsComplete() {
// This will update ALL rows in the todos table, ignoring RLS
const { data, error } = await supabaseAdmin
.from('todos')
.update({ is_complete: true })
.neq('is_complete', true); // Only update if not already complete
if (error) console.error('Error marking todos complete:', error);
return data;
}
The key difference is the absence of RLS enforcement when using the service role. This is powerful but dangerous if misused. Always ensure that your service role key is kept secret and only used in trusted backend environments.
When you initialize your Supabase client, you’re essentially telling it which "identity" it should operate under. The anon key is tied to an unauthenticated or authenticated end-user identity, governed by RLS. The service role key is tied to a super-user identity with elevated privileges, bypassing RLS entirely for administrative or backend tasks.
The most surprising true thing about Supabase’s anon and service role keys is that the anon key isn’t truly anonymous; it’s tied to the current user’s session, which is managed via JWTs, and RLS policies are evaluated against auth.uid() derived from that JWT.
The service role key, when used, essentially tells Supabase to ignore any JWT present in the request. It operates with the full permissions of the database user associated with the Supabase project, meaning it has carte blanche over your tables unless you’ve implemented database-level restrictions (like SECURITY DEFINER functions, which are a more advanced topic).
The practical implication is that any operation performed with the service role key will succeed or fail based solely on database permissions, not on the RLS policies you’ve set up for your application users. This is why it’s critical to never expose the service role key to the client. If a malicious actor obtained your service role key, they could read, write, or delete any data in your database without any RLS checks.
When you’re building out complex backend logic or integrations, you’ll often find yourself reaching for the service role key to perform tasks that involve system-wide changes or data manipulation that doesn’t fit within the context of a single user’s permissions. However, always consider if RLS could be used first, even if it requires a more intricate policy, before resorting to the service role.
The next concept you’ll likely encounter is managing different database schemas for distinct parts of your application or for different environments.