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
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
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
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:
- Next.js + Supabase + Vercel - Next.js alternative
- SvelteKit + Supabase - Svelte alternative
- React + Supabase - Client-only SPA version