How to Use Supabase with Astro: Complete Guide
Step-by-step guide to integrating Supabase with your Astro website. Installation, configuration, and best practices.
Supabase is the open-source Firebase alternative built on PostgreSQL. It gives you a database, authentication, realtime subscriptions, and storage out of the box. When you pair it with Astro, you get a full-stack setup where static pages load instantly and server-side routes handle dynamic data. It is one of the most practical backends you can add to an Astro project.
This guide is written and verified against Astro 6.4.2 and the current Supabase docs (checked 2026-05-29). One thing to flag up front, because it trips up older tutorials. The output: 'hybrid' mode was removed in Astro 5, and Astro is now on version 6. The default output is static. You add an adapter and opt individual routes into server rendering with export const prerender = false. The sections below use that current approach.
Prerequisites
- Node.js 22.12.0 or newer (Astro 6 dropped Node 18 and Node 20 support entirely and now requires Node 22.12.0+)
- An Astro project (
npm create astro@latest) - A Supabase account and project (free tier at supabase.com)
Installation
Install the Supabase JavaScript client:
npm install @supabase/supabase-js@2.106.2
At the time of writing, @supabase/supabase-js is at 2.106.2. The official Astro guide installs only this package for build-time data fetching and basic queries. If you plan to do cookie-based server-side auth, you also want @supabase/ssr (covered in the Authentication section below).
Configuration
Grab your project URL and anon key from the Supabase dashboard under Settings > API. Add them to your .env file:
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_ANON_KEY=your_anon_key_here
Create a client helper:
// src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
import.meta.env.SUPABASE_URL,
import.meta.env.SUPABASE_ANON_KEY
);
The official Astro guide accesses these through import.meta.env.SUPABASE_URL and import.meta.env.SUPABASE_ANON_KEY, and recommends adding type definitions in src/env.d.ts for editor autocomplete:
// src/env.d.ts
interface ImportMetaEnv {
readonly SUPABASE_URL: string;
readonly SUPABASE_ANON_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Output Mode and Adapters (Astro 5 and 6)
Here is the part many older guides get wrong. The output: 'hybrid' value was removed in Astro 5, so do not use it. In current Astro the default output is static, which prerenders every page to HTML at build time. To run server-side routes (API endpoints, auth handlers, request-time Supabase queries) you do two things.
First, install an adapter. For a self-hosted Node server:
npx astro add node
That command installs @astrojs/node (currently 10.1.2) and wires up your config for you. The result looks like this:
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
adapter: node({
mode: "standalone",
}),
});
Second, opt the routes that need server rendering out of prerendering with export const prerender = false. With the default static output, individual pages and endpoints stay static unless you set this flag:
// src/pages/api/subscribe.ts
export const prerender = false;
If most of your site is dynamic, you can flip the default by setting output: 'server' in astro.config.mjs, then opt static pages back in with export const prerender = true. For a content blog with a few dynamic routes, leaving the default static and marking only the dynamic endpoints with prerender = false is the cleaner choice.
Basic Usage
Let's fetch a list of blog posts from a Supabase table and render them on a static page:
---
// src/pages/blog/index.astro
import { supabase } from "../../lib/supabase";
import BaseLayout from "../../layouts/BaseLayout.astro";
const { data: posts, error } = await supabase
.from("posts")
.select("id, title, slug, excerpt, created_at")
.eq("published", true)
.order("created_at", { ascending: false });
if (error) {
console.error("Error fetching posts:", error.message);
}
---
<BaseLayout title="Blog">
<h1>Blog</h1>
<ul>
{posts?.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</a>
</li>
))}
</ul>
</BaseLayout>
Building an API Endpoint
Astro's server endpoints work perfectly with Supabase for handling form submissions, authentication, or any dynamic data. Remember the export const prerender = false line so the endpoint runs on request instead of being baked at build time:
// src/pages/api/subscribe.ts
import type { APIRoute } from "astro";
import { supabase } from "../../lib/supabase";
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const formData = await request.json();
const { email } = formData;
if (!email) {
return new Response(JSON.stringify({ error: "Email is required" }), {
status: 400,
});
}
const { error } = await supabase
.from("subscribers")
.insert({ email });
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
return new Response(JSON.stringify({ success: true }), { status: 200 });
};
Authentication with Supabase
Supabase Auth integrates smoothly with Astro's server rendering. For cookie-based sessions, Supabase now recommends the @supabase/ssr package, which handles reading and writing the auth cookies for you. The older @supabase/auth-helpers packages are deprecated (npm marks them "no longer supported"), so use @supabase/ssr on new projects:
npm install @supabase/supabase-js@2.106.2 @supabase/ssr@0.10.3
One caveat worth knowing. @supabase/ssr is still pre-1.0 (0.10.3 at the time of writing), so under semver its API can change between minor versions. Pin the version you build against (0.10.3 here) and read the release notes before bumping.
Create a request-scoped server client that bridges Supabase's cookie handling to Astro's cookies API:
// src/lib/supabase-server.ts
import { createServerClient, parseCookieHeader } from "@supabase/ssr";
import type { AstroCookies } from "astro";
export function createSupabaseServerClient(
cookies: AstroCookies,
headers: Headers
) {
return createServerClient(
import.meta.env.SUPABASE_URL,
import.meta.env.SUPABASE_ANON_KEY,
{
cookies: {
getAll() {
return parseCookieHeader(headers.get("Cookie") ?? "");
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
cookies.set(name, value, options);
});
},
},
}
);
}
Then a basic login endpoint. The @supabase/ssr client writes the session cookies itself through the setAll callback above, so you do not set tokens by hand:
// src/pages/api/auth/login.ts
import type { APIRoute } from "astro";
import { createSupabaseServerClient } from "../../../lib/supabase-server";
export const prerender = false;
export const POST: APIRoute = async ({ request, cookies }) => {
const { email, password } = await request.json();
const supabase = createSupabaseServerClient(cookies, request.headers);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 401,
});
}
return new Response(JSON.stringify({ success: true }), { status: 200 });
};
If you only need build-time data fetching and not logged-in sessions, you can skip @supabase/ssr entirely and stick with the plain @supabase/supabase-js client from the Configuration section.
Production Tips
Use Row Level Security (RLS). Always enable RLS on your tables. The anon key is exposed to the client, so RLS policies are your real access control layer.
Use the service role key server-side only. For admin operations in API routes, create a separate Supabase client with the service role key. Never expose it to the browser.
Generate TypeScript types. Run
supabase gen types typescript --linked > src/database.types.tswith the Supabase CLI (currently 2.102.0) to generate types from your database schema, then pass them as the generic tocreateClient<Database>(...). This gives you full autocomplete and catches errors at build time.
Enable realtime only where needed. Realtime subscriptions add overhead. Only enable them on tables that actually need live updates.
Use Supabase Edge Functions for webhooks. Instead of polling, set up database triggers that call Supabase Edge Functions to handle events like new signups or order completions.
Alternatives to Consider
- Firebase if you are already in the Google ecosystem and prefer a NoSQL document database with tight mobile SDK support.
- Neon if you want serverless Postgres without the extra features (auth, storage). Lighter weight, just the database.
- PlanetScale if you need a MySQL-compatible serverless database with branching for schema changes.
Common Errors and Fixes
output: "hybrid" is not a valid value. Hybrid mode was removed in Astro 5, and the current major is Astro 6. Delete the output: "hybrid" line. The default static output already lets you mix static pages with server routes, as long as you add an adapter and mark dynamic routes with export const prerender = false.
Server endpoint returns 404 or runs at build time. If an API route or auth handler is not responding on request, you are almost certainly missing export const prerender = false in that file, or you have not added an adapter. With the default static output, anything without that flag gets prerendered to a fixed value at build. Per the Astro on-demand rendering guide, add the adapter first, then opt the route out of prerendering.
No adapter installed. Building a project that uses server rendering without an adapter fails with an error telling you to add one. Run npx astro add node (or the adapter for your host) to install @astrojs/node and update the config in one step.
Env vars come back undefined. In Astro, server-side env vars are read with import.meta.env.SUPABASE_URL, not process.env. Confirm the variable is in .env, restart the dev server (Astro only reads .env at startup), and add the ImportMetaEnv interface in src/env.d.ts so TypeScript stops complaining.
@supabase/auth-helpers shows a deprecation warning. The auth-helpers packages are no longer supported on npm. Migrate to @supabase/ssr and the createServerClient pattern shown above.
@supabase/ssr breaks after a minor bump. The package is still pre-1.0 (0.10.3), so under semver a minor bump can carry breaking changes. Pin the exact version and review release notes before upgrading.
Anon key queries return empty arrays. If a query that should return rows comes back empty with no error, Row Level Security is almost always the cause. The anon key respects RLS, so add a policy that allows the read, or run privileged work through a service-role client server-side only.
Official Docs and Examples
- Astro official Supabase backend guide: https://docs.astro.build/en/guides/backend/supabase/
- Astro on-demand rendering and output modes: https://docs.astro.build/en/guides/on-demand-rendering/
- Astro Node adapter (
@astrojs/node): https://docs.astro.build/en/guides/integrations-guide/node/ - Supabase server-side auth, creating a client with
@supabase/ssr: https://supabase.com/docs/guides/auth/server-side/creating-a-client - Community example repo (Astro + Supabase auth and database demo): https://github.com/kevinzunigacuellar/astro-supabase
Wrapping Up
Supabase gives you almost everything you need for a backend in one package, and Astro's static-by-default model with per-route prerender control means you can use it for both build-time data fetching and on-demand API routes. The free tier is generous enough to build a real product on before you ever need to pay.
Sources
All versions and recommendations below were checked on 2026-05-29.
@supabase/supabase-js2.106.2, https://registry.npmjs.org/@supabase/supabase-js/latest@supabase/ssr0.10.3, https://registry.npmjs.org/@supabase/ssr/latestastro6.4.2, https://registry.npmjs.org/astro/latest@astrojs/node10.1.2, https://registry.npmjs.org/@astrojs/node/latestsupabaseCLI 2.102.0, https://registry.npmjs.org/supabase/latest@supabase/auth-helpers-nextjsdeprecation ("Package no longer supported"), https://registry.npmjs.org/@supabase/auth-helpers-nextjs/latest- Astro official Supabase guide (install command,
import.meta.envaccess,output: 'server'/prerenderguidance), https://docs.astro.build/en/guides/backend/supabase/ - Astro on-demand rendering (default
static, nohybrid, adapter +prerenderflags), https://docs.astro.build/en/guides/on-demand-rendering/ - Astro Node adapter (
npx astro add node, standalone config), https://docs.astro.build/en/guides/integrations-guide/node/ - Supabase server-side client (
@supabase/ssr,createServerClient), https://supabase.com/docs/guides/auth/server-side/creating-a-client - Community example repo, https://github.com/kevinzunigacuellar/astro-supabase
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.