Remix + Supabase Security Blueprint

Share

To secure a Remix + Supabase stack, you need to: (1) create a Supabase client per request for proper cookie handling, (2) verify auth using getUser() in all loaders and actions, (3) enable RLS as defense-in-depth, (4) store sessions in cookies with proper Set-Cookie headers, and (5) use verified user IDs from auth (not form data). This blueprint covers Remix's server-first patterns with Supabase SSR.

TL;DR

Remix's loader/action pattern pairs well with Supabase. Key tasks: create Supabase client per request, use getUser() to verify auth in loaders and actions, enable RLS as defense-in-depth, and store session in cookies. Remix runs on the server, giving you more control than pure SPAs.

Supabase Client Per Request Supabase Remix

app/lib/supabase.server.ts
import { createServerClient } from '@supabase/ssr'

export function createSupabaseClient(request: Request) {
  const cookies = parse(request.headers.get('Cookie') ?? '')
  const headers = new Headers()

  const supabase = createServerClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(key) { return cookies[key] },
        set(key, value, options) {
          headers.append('Set-Cookie', serialize(key, value, options))
        },
        remove(key, options) {
          headers.append('Set-Cookie', serialize(key, '', options))
        },
      },
    }
  )

  return { supabase, headers }
}

Auth in Loaders Remix

app/routes/dashboard.tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const { supabase, headers } = createSupabaseClient(request)

  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    throw redirect('/login', { headers })
  }

  const { data: posts } = await supabase
    .from('posts')
    .select('*')
    .eq('author_id', user.id)

  return json({ posts }, { headers })
}

Auth in Actions Remix

Secure action pattern
export async function action({ request }: ActionFunctionArgs) {
  const { supabase, headers } = createSupabaseClient(request)

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) {
    throw new Response('Unauthorized', { status: 401, headers })
  }

  const formData = await request.formData()

  await supabase.from('posts').insert({
    title: formData.get('title'),
    author_id: user.id  // Use verified user ID
  })

  return redirect('/dashboard', { headers })
}

Security Checklist

Pre-Launch Checklist

RLS enabled on all tables

Auth verified in all loaders/actions

Supabase client created per request

Cookie headers properly returned

Environment variables configured

Alternative Stacks

Consider these related blueprints:

Building with this stack?

Scan for RLS and auth issues.

Start Free Scan
Security Blueprints

Remix + Supabase Security Blueprint