Jamstack + Supabase Security Blueprint

Share

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

src/lib/supabase.js
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

supabase/migrations/rls.sql
-- 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

supabase/functions/admin-action/index.ts
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:

Security Blueprints

Jamstack + Supabase Security Blueprint