To secure a Jamstack + Supabase site, you need to: (1) enable RLS on ALL tables since Jamstack sites have no server layer, (2) use only the anon key in client-side code, (3) leverage Edge Functions for operations requiring the service key, (4) test RLS policies thoroughly in the SQL editor, and (5) configure Storage bucket policies for file uploads. This blueprint covers pure client-side security patterns with Edge Functions for privileged operations.
TL;DR
Jamstack sites with Supabase have no server layer-100% of your security depends on RLS. Enable RLS on every table with restrictive policies, use anon key only, and leverage Edge Functions for operations requiring the service key.
Client-Side Supabase Setup Supabase
import { createClient } from '@supabase/supabase-js'
// Only use the anon key on the client
export const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
)
Row Level Security Supabase
-- Enable RLS on all tables
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
-- Public read, authenticated write
CREATE POLICY "Public read posts"
ON posts FOR SELECT
USING (published = true);
CREATE POLICY "Authors manage own posts"
ON posts FOR ALL
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Comments: authenticated users only
CREATE POLICY "Auth users read comments"
ON comments FOR SELECT
USING (auth.uid() IS NOT NULL);
CREATE POLICY "Auth users create own comments"
ON comments FOR INSERT
WITH CHECK (auth.uid() = user_id);
Edge Function for Service Key Operations Supabase
import { createClient } from '@supabase/supabase-js'
Deno.serve(async (req) => {
// Verify user auth from request
const authHeader = req.headers.get('Authorization')
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader! } } }
)
const { data: { user } } = await supabaseClient.auth.getUser()
if (!user) {
return new Response('Unauthorized', { status: 401 })
}
// Use service key for admin operations
const adminClient = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Perform privileged operation
const { data } = await adminClient.from('posts').select('*')
return new Response(JSON.stringify(data))
})
RLS is your only security layer. With no server, clients access the database directly. Every table needs RLS. Test policies thoroughly before launch.
Security Checklist
Pre-Launch Checklist
RLS enabled on ALL tables
RLS policies tested in SQL editor
Only anon key in client code
Service key in Edge Functions only
Storage bucket policies configured
Alternative Stacks
Consider these related blueprints:
- Astro + Supabase - With hybrid SSR support
- Next.js + Supabase + Vercel - Full-stack with server rendering
- React + Supabase - React SPA alternative