Astro + Supabase Security Blueprint

Share

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

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

src/pages/dashboard.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

src/pages/api/posts.ts
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:

Security Blueprints

Astro + Supabase Security Blueprint