To secure an Astro + Supabase site, you need to: (1) understand your render mode (static vs SSR) since security patterns differ, (2) verify auth server-side using getUser() in SSR pages and API routes, (3) enable RLS on all tables as defense-in-depth, (4) use access tokens from cookies for server-side auth, and (5) use the anon key only (never service_role on client). This blueprint covers Astro's hybrid rendering with Supabase patterns.
TL;DR
Astro's hybrid rendering means security depends on your output mode. For static pages, rely on RLS. For SSR pages and API routes, verify auth server-side. Use @astrojs/vercel or similar adapters for server functionality and always enable RLS as defense in depth.
Supabase Client Configuration Supabase Astro
import { createClient } from '@supabase/supabase-js'
// Client-side (anon key only)
export const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
)
// Server-side (for API routes)
export function createServerClient(accessToken?: string) {
return createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
: {},
},
}
)
}
Protected SSR Page Astro
---
export const prerender = false // Enable SSR
import { createServerClient } from '../lib/supabase'
const accessToken = Astro.cookies.get('sb-access-token')?.value
if (!accessToken) {
return Astro.redirect('/login')
}
const supabase = createServerClient(accessToken)
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return Astro.redirect('/login')
}
const { data: posts } = await supabase
.from('posts')
.select('*')
.eq('user_id', user.id)
---
<html>
<body>
<h1>Dashboard</h1>
{posts?.map(post => <article>{post.title}</article>)}
</body>
</html>
API Endpoint Astro
import type { APIRoute } from 'astro'
import { createServerClient } from '../../lib/supabase'
export const POST: APIRoute = async ({ request, cookies }) => {
const accessToken = cookies.get('sb-access-token')?.value
if (!accessToken) {
return new Response('Unauthorized', { status: 401 })
}
const supabase = createServerClient(accessToken)
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return new Response('Unauthorized', { status: 401 })
}
const body = await request.json()
const { data, error } = await supabase.from('posts').insert({
title: body.title,
user_id: user.id,
}).select().single()
if (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 400 })
}
return new Response(JSON.stringify(data), { status: 201 })
}
Know your render mode. Static pages (default) have no server-security is 100% RLS. SSR pages and API routes run server-side but still need RLS as defense in depth.
Security Checklist
Pre-Launch Checklist
RLS enabled on all tables
Auth verified in SSR pages
Auth verified in API routes
getUser() used for verification
Output mode understood per page
Alternative Stacks
Consider these related blueprints:
- Jamstack + Supabase - Pure static approach
- Next.js + Supabase + Vercel - React/Next.js alternative
- SvelteKit + Supabase - Svelte alternative