name: supabase-cloud-rls-bypass-edge-function
description: Bypass Supabase Cloud RLS 401 errors by using Edge Functions with service_role key
Supabase Cloud: Bypass RLS with Edge Function
When to Use
When a Supabase Cloud table has Row Level Security (RLS) enabled and your frontend queries return 401 Unauthorized even with a valid anon key. This commonly happens when:
- - A new table is created without explicit RLS policies
- - The
anonkey is blocked by default Supabase RLS rules - - You need to query a table from client-side code without requiring the user to be authenticated
- - The operation is simple (one INSERT or SELECT)
- - You don't want to deploy/maintain an Edge Function
- - You're already storing the PAT in your frontend code
- - The PAT is stored server-side (e.g., in environment variables read by SSR)
- - The operation is low-risk (inserting public link tokens)
- - The table has no sensitive data that could be exposed via SQL injection
- - Values are validated before interpolation (e.g., token is a SHA256 hex hash — safe by nature)
- - Management API
database/querydoes NOT support $1/$2 parameter placeholders — always use direct string interpolation with.replace(/'/g, "''")for escaping - - Supabase CLI
db queryfails with env var errors when not locally linked — use the Edge Function approach instead - - The Edge Function runs on Deno, not Node.js — use
Deno.env.get()notprocess.env - - Always use
--no-verify-jwton deployed Edge Functions when called from frontend with anon key - -
service_rolekey bypasses ALL RLS — never expose it to the client; only use in Edge Functions - - When RLS is enabled but no policies exist (
pg_policiesreturns[]), ALL operations are denied silently — no error thrown, just empty result - - Tokens that are SHA256 hashes (64 hex chars) are safe for direct SQL interpolation — no risk of injection
The Pattern
DON'T try to create RLS policies via CLI (they fail with SUPABASE_DB_PASSWORD env var error).
DO create a Supabase Edge Function that uses the service_role key (bypasses RLS) and call it from the frontend with the anon key as Bearer token.
Implementation
1. Create the Edge Function
`typescript
// supabase/functions/check-allowlist/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
Deno.serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const { email } = await req.json();
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
const { data, error } = await supabase
.from('allowed_emails')
.select('id')
.eq('email', email.toLowerCase().trim())
.maybeSingle();
return new Response(JSON.stringify({ allowed: !!data }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (err) {
return new Response(JSON.stringify({ error: 'Internal error' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
`
2. Deploy with --no-verify-jwt
`bash
supabase functions deploy check-allowlist --no-verify-jwt
`
3. Call from frontend using anon key
`typescript
async function checkAllowlist(email: string) {
const res = await fetch('https://
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer
},
body: JSON.stringify({ email }),
});
const result = await res.json();
return result.allowed;
}
`
Alternative: Management API (no Edge Function needed)
For simple INSERT/SELECT operations, you can bypass RLS without creating an Edge Function by using the Supabase Management API directly from browser code with a Personal Access Token (PAT).
When to use this instead:
⚠️ CRITICAL: Management API does NOT support parameterized queries ($1, $2)
The database/query endpoint only accepts raw SQL strings — no placeholders. Always interpolate values directly and escape single quotes with .replace(/'/g, "''").
`typescript
// ✅ CORRECT — direct interpolation with escape
query: SELECT * FROM client_upload_links WHERE token = '${token.trim().replace(/'/g, "''")}'
// ❌ WRONG — $1 placeholders are NOT supported by Management API
query: SELECT * FROM client_upload_links WHERE token = $1,
params: [token] // ← This will fail with "there is no parameter $1"
`
Complete example (insert with safe interpolation):
`typescript
const res = await fetch(
https://api.supabase.com/v1/projects/${PROJECT_REF}/database/query,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: Bearer ${PAT},
},
body: JSON.stringify({
query: INSERT INTO public.client_upload_links (id, client_id, token, folder_id, expires_at, created_at) VALUES (gen_random_uuid(), '${clientId}', '${token.replace(/'/g, "''")}', '${folderId}', '${expiresAt}', now()) RETURNING id,
}),
}
);
const data = await res.json();
`
Security note: The Management API PAT gives full database admin access. Only use this pattern when:
Key differences from Edge Function approach:
| Edge Function | Management API | |
| --- | --- | --- |
| Requires deploy | Yes (supabase functions deploy) | No |
| Supports $1 placeholders | ✅ Yes | ❌ No — use direct interpolation |
| Bypass mechanism | service_role key | PAT has full admin access |
| Latency | Higher (cold start) | Lower |
| Best for | Complex queries, multiple tables | Simple inserts/selects |
| Security | Service role stays server-side | PAT in browser (less secure) |
Key Lessons
Project Context
Used for: DocCorretor (documentos.rochasalesseguros.com.br) allowlist system at /var/www/documentos/deploy-vps-nodejs/app/