Server-side auth
Tested with: @supabase/ssr 0.7 · supabase-js 2 · Next.js 15 · SvelteKit 2
@supabase/ssr keeps the session in cookies so your server can render pages for the signed-in user and call the Data API
with their token, under row-level security. It works with Databasezy unchanged: the cookies are written by the library,
and the project endpoint serves what it needs.
What the project endpoint provides
Section titled “What the project endpoint provides”- PKCE code exchange: sign-in with a provider, magic links, sign-up confirmations and password resets started with
flowType: "pkce"(the @supabase/ssr default) come back to your site as?code=...; your server callsexchangeCodeForSession(code), which usesPOST /auth/v1/token?grant_type=pkcewith the verifier the library kept in a cookie. - Token hashes in email templates:
{{ .TokenHash }}(and{{ .RedirectTo }}, always one of your allowed URLs) lets a template link straight to your server:{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email, whereverifyOtp({ token_hash, type })signs the user in. - CORS on
/auth/v1for the browser client (createBrowserClient).
Set the site URL and the redirect URLs (Platform → Auth → URL configuration) to your app’s callback routes, for
example http://localhost:3000/** in development and https://app.example.com/** in production.
Set up
Section titled “Set up”npm install @supabase/supabase-js @supabase/ssrDATABASEZY_URL=https://<ref>.us-east.databasezy.com:8443DATABASEZY_PUBLISHABLE_KEY=zbp_...import { createServerClient } from "@supabase/ssr";import { cookies } from "next/headers";
export async function createClient() { const cookieStore = await cookies(); return createServerClient(process.env.NEXT_PUBLIC_DATABASEZY_URL!, process.env.NEXT_PUBLIC_DATABASEZY_PUBLISHABLE_KEY!, { cookies: { getAll: () => cookieStore.getAll(), setAll: (list) => { try { for (const { name, value, options } of list) cookieStore.set(name, value, options); } catch { // Server Components cannot set cookies; the middleware refreshes them. } }, }, });}import { createServerClient } from "@supabase/ssr";import { type NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) { let response = NextResponse.next({ request }); const supabase = createServerClient(process.env.NEXT_PUBLIC_DATABASEZY_URL!, process.env.NEXT_PUBLIC_DATABASEZY_PUBLISHABLE_KEY!, { cookies: { getAll: () => request.cookies.getAll(), setAll: (list) => { for (const { name, value } of list) request.cookies.set(name, value); response = NextResponse.next({ request }); for (const { name, value, options } of list) response.cookies.set(name, value, options); }, }, }); await supabase.auth.getUser(); // refreshes the session cookies when needed return response;}
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };import { NextResponse } from "next/server";import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); if (code) { const supabase = await createClient(); const { error } = await supabase.auth.exchangeCodeForSession(code); if (!error) return NextResponse.redirect(`${origin}/notes`); } return NextResponse.redirect(`${origin}/login?error=auth`);}import { createServerClient } from "@supabase/ssr";import type { Handle } from "@sveltejs/kit";import { env } from "$env/dynamic/public";
export const handle: Handle = async ({ event, resolve }) => { event.locals.supabase = createServerClient(env.PUBLIC_DATABASEZY_URL, env.PUBLIC_DATABASEZY_PUBLISHABLE_KEY, { cookies: { getAll: () => event.cookies.getAll(), setAll: (list) => list.forEach(({ name, value, options }) => event.cookies.set(name, value, { ...options, path: "/" })), }, }); event.locals.safeGetSession = async () => { const { data: { user }, error } = await event.locals.supabase.auth.getUser(); if (error || !user) return { session: null, user: null }; const { data: { session } } = await event.locals.supabase.auth.getSession(); return { session, user }; }; return resolve(event, { filterSerializedResponseHeaders: (name) => name === "content-range" || name === "x-supabase-api-version" });};import { redirect } from "@sveltejs/kit";
export const GET = async ({ url, locals }) => { const code = url.searchParams.get("code"); if (code && !(await locals.supabase.auth.exchangeCodeForSession(code)).error) redirect(303, "/notes"); redirect(303, "/login?error=auth");};import { createServerClient, parseCookieHeader, serializeCookieHeader } from "@supabase/ssr";
export function supabaseFor(request: Request) { const headers = new Headers(); const supabase = createServerClient(process.env.DATABASEZY_URL!, process.env.DATABASEZY_PUBLISHABLE_KEY!, { cookies: { getAll: () => parseCookieHeader(request.headers.get("Cookie") ?? ""), setAll: (list) => list.forEach(({ name, value, options }) => headers.append("Set-Cookie", serializeCookieHeader(name, value, options))), }, }); return { supabase, headers }; // return `headers` from your loader / action}import { createServerClient, parseCookieHeader } from "@supabase/ssr";import type { H3Event } from "h3";
export function supabaseFor(event: H3Event) { const config = useRuntimeConfig(); return createServerClient(config.public.databasezyUrl, config.public.databasezyPublishableKey, { cookies: { getAll: () => parseCookieHeader(getHeader(event, "cookie") ?? ""), setAll: (list) => list.forEach(({ name, value, options }) => setCookie(event, name, value, options)), }, });}@nuxtjs/supabase works too: set url and key to the project endpoint and the publishable key.
import { createServerClient, parseCookieHeader } from "@supabase/ssr";import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => { context.locals.supabase = createServerClient(import.meta.env.PUBLIC_DATABASEZY_URL, import.meta.env.PUBLIC_DATABASEZY_PUBLISHABLE_KEY, { cookies: { getAll: () => parseCookieHeader(context.request.headers.get("Cookie") ?? ""), setAll: (list) => list.forEach(({ name, value, options }) => context.cookies.set(name, value, options)), }, }); return next();});Email links straight to your server
Section titled “Email links straight to your server”Change the confirmation, magic link and recovery templates (Platform → Auth → Email templates) to link to a route of yours:
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next=/notes">Sign in</a>import { type EmailOtpType } from "@supabase/supabase-js";import { NextResponse } from "next/server";import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const token_hash = searchParams.get("token_hash"); const type = searchParams.get("type") as EmailOtpType | null; if (token_hash && type) { const supabase = await createClient(); if (!(await supabase.auth.verifyOtp({ token_hash, type })).error) return NextResponse.redirect(`${origin}/notes`); } return NextResponse.redirect(`${origin}/login?error=link`);}Example apps
Section titled “Example apps”Complete, buildable examples live in the repository: Next.js and SvelteKit. Each signs in (password and magic link), refreshes the session in middleware and reads rows protected by row-level security on the server.
Security notes
Section titled “Security notes”- On the server, decide on
getUser()(orgetClaims(), which verifies the JWT against the project’s JWKS), never ongetSession(), which only reads the cookie. - @supabase/ssr’s cookies are readable by JavaScript by design: keep your site free of XSS (a Content-Security-Policy helps) and keep sessions short in Auth → Sessions if you handle sensitive data.
- Only the publishable key goes to the browser. The secret key bypasses row-level security; keep it in server-only code.
- Keep the redirect allow-list tight: links and OAuth flows return only to your site URL or listed URLs.
- See OAuth 2.1 and OpenID Connect server to let other apps sign in with your project.