Skip to content

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.

  • 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 calls exchangeCodeForSession(code), which uses POST /auth/v1/token?grant_type=pkce with 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, where verifyOtp({ token_hash, type }) signs the user in.
  • CORS on /auth/v1 for 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.

shell
npm install @supabase/supabase-js @supabase/ssr
.env
DATABASEZY_URL=https://<ref>.us-east.databasezy.com:8443
DATABASEZY_PUBLISHABLE_KEY=zbp_...
utils/supabase/server.ts
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.
}
},
},
});
}
middleware.ts
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).*)"] };
app/auth/callback/route.ts
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`);
}

Change the confirmation, magic link and recovery templates (Platform → Auth → Email templates) to link to a route of yours:

Magic link template
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next=/notes">Sign in</a>
app/auth/confirm/route.ts (Next.js)
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`);
}

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.

  • On the server, decide on getUser() (or getClaims(), which verifies the JWT against the project’s JWKS), never on getSession(), 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.