Quick Answer
AI tools generate Supabase Edge Functions that use the service role key (bypasses all Row Level Security), skip input validation, return full database rows including sensitive fields, and lack proper CORS configuration. The service role key is the master key to your database - if exposed in client-side code or logs, an attacker has full read/write access to every table. Always use the anon key with RLS for user requests and validate every input with Zod.
The Service Key Problem
Supabase provides two keys: the anon key (respects Row Level Security) and the service role key (bypasses all RLS policies). Newer projects call them the publishable key and the secret key; the legacy keys used below still work, but Supabase is deprecating them by the end of 2026. AI code generators almost always use the service role key because it "just works" - no RLS policies to configure, no permission errors to debug. But the service role key is an admin credential: Supabase says it has full access to your project's data and skips every RLS policy.
A Supabase service key bypasses row level security. Once exposed, an attacker can read, modify, and delete every row in every table. A common mistake is using the service key in places where the anon key would do.
// ❌ BAD - Service role key in Edge Function (bypasses all RLS)
import { createClient } from 'npm:@supabase/supabase-js@2';
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // Full database access!
);
Deno.serve(async (req) => {
const { userId } = await req.json();
// No auth check - anyone can read any user's data
const { data } = await supabase
.from('users')
.select('*') // Returns password_hash, internal fields
.eq('id', userId);
return new Response(JSON.stringify(data));
});
// ✅ GOOD - User's JWT token creates an authenticated client with RLS
import { createClient } from 'npm:@supabase/supabase-js@2';
Deno.serve(async (req) => {
// Extract user's JWT from the Authorization header
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response('Unauthorized', { status: 401 });
}
// Create a client that respects RLS using the user's token
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
);
// RLS policies automatically restrict to the user's own data
const { data } = await supabase
.from('profiles')
.select('id, name, avatar_url'); // Only safe fields
return new Response(JSON.stringify(data));
});
When the Service Key IS Needed
The service role key has legitimate uses in Edge Functions, but only for operations that should bypass RLS by design:
| Use Case | Key to Use | Why |
|---|---|---|
| User reads their own data | Anon key + user JWT | RLS restricts access to own rows |
| User updates their profile | Anon key + user JWT | RLS ensures they edit only their own data |
| Webhook from Stripe/GitHub | Service role key | External webhooks have no user JWT |
| Admin batch operation | Service role key | Admin operations cross user boundaries |
| Scheduled cleanup job | Service role key | Background jobs have no user context |
Input Validation in Edge Functions
Edge Functions receive raw HTTP requests. AI tools generate code that trusts every field in the request body without validation, which enables injection attacks and unexpected behavior.
// ❌ BAD - No validation, trusts all input
Deno.serve(async (req) => {
const { email, role, credits } = await req.json();
await supabase.from('users').insert({ email, role, credits });
return new Response('Created');
});
// Attacker sends: { "email": "a@b.com", "role": "admin", "credits": 999999 }
// ✅ GOOD - Validate every field, whitelist allowed values
import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts';
const createUserSchema = z.object({
email: z.string().email().max(255),
// 'role' and 'credits' are NOT accepted from client
});
Deno.serve(async (req) => {
const body = await req.json();
const data = createUserSchema.parse(body);
await supabase.from('users').insert({
email: data.email,
role: 'user', // Server-defined default
credits: 0, // Server-defined default
});
return new Response(JSON.stringify({ success: true }));
});
CORS Configuration Mistakes
AI-generated Edge Functions either skip CORS entirely (breaking browser requests) or use * for all origins (allowing any website to call your API):
// ❌ BAD - Allows any origin
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
};
// ✅ GOOD - Restrict to your domains
const ALLOWED_ORIGINS = [
'https://yourdomain.com',
'https://app.yourdomain.com',
];
function getCorsHeaders(req: Request) {
const origin = req.headers.get('Origin') ?? '';
const allowedOrigin = ALLOWED_ORIGINS.includes(origin) ? origin : '';
return {
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
}
Edge Function Security Checklist
- Use anon key + user JWT for user-facing requests - let RLS do the authorization
- Reserve service role key for webhooks, admin jobs, and background tasks only
- Validate all input with Zod - never trust request body fields
- Never accept role/permission fields from the client
- Select specific columns - never return
SELECT * - Configure CORS properly - whitelist your domains, not
* - Verify webhook signatures - Stripe, GitHub, and others sign their payloads
- Handle errors safely - never return stack traces or database errors to the client
Tools like VibeDoctor's Vibe Check flag hardcoded keys and tokens, including a service role key pasted into source code, and variables with SECRET, PRIVATE, or PASSWORD in their name exposed through prefixes like NEXT_PUBLIC_ or VITE_. Review the CORS headers and input validation in your Edge Functions by hand. Free to sign up.
FAQ
Is it safe to expose the Supabase anon key?
Yes. The anon key is designed to be used in client-side code. It only grants access that your Row Level Security policies allow. On a table with RLS enabled and no policies, the anon key gets no access at all; on a table with RLS disabled, it can read and write every row. With properly configured RLS, it lets authenticated users access only their own data. The danger is when you use the service role key instead, which bypasses all RLS.
How do I know if my Edge Function is using the wrong key?
Search your code for SUPABASE_SERVICE_ROLE_KEY. If it appears in any Edge Function that handles user requests (not webhooks or admin jobs), it should be replaced with the anon key + user JWT pattern. The service key should only appear in server-side code that intentionally needs to bypass RLS.
Can I use Row Level Security with Edge Functions?
Yes. When you create a Supabase client with the anon key and pass the user's JWT in the Authorization header, all queries respect your RLS policies automatically. This is the recommended pattern for user-facing Edge Functions. It means your function does not need to manually check permissions - the database enforces them.
What if my Edge Function needs both RLS and service-level access?
Create two Supabase clients: one with the anon key + user JWT (for user-scoped queries) and one with the service role key (for admin operations within the same function). Use the user client for reading/writing user data and the service client only for cross-user operations like incrementing global counters or sending notifications.
Do Supabase Edge Functions have built-in rate limiting?
Not for incoming requests. Supabase rate limits calls one Edge Function makes to another, but inbound requests to your Edge Functions are not subject to that limit. Add application-level rate limiting inside your Edge Functions for sensitive operations like authentication, payments, and data writes. Use a counter in your database or Redis for cross-request tracking; Supabase's own rate limiting example uses Upstash Redis.