Series: supabase-series · Part 5
Dev Productivity & Tools
Auth: JWT, session and backend integration
Supabase Auth, JWT; verify JWT on backend (NestJS, Express, FastAPI). Session and refresh. Secrets server-side only.
2026-03-173 min read
- 0.Series: Supabase from setup to deploy
- 1.Supabase overview
- 2.Project setup, CLI and environment variables
- 3.Postgres schema and table design
- 4.Migrations: write and apply
- 5.Auth: JWT, session and backend integration(this post)
- 6.Prisma + Supabase: connect and sync schema
- 7.Row Level Security (RLS) and policies
- 8.API: PostgREST vs custom API and when to use which
- 10.Deploy, CI/CD and multi-project
Introduction
Supabase Auth provides sign-up/sign-in (email, OAuth, magic link), session management, and issues a JWT. The client sends the JWT in a header when calling PostgREST or your own API; the backend verifies the JWT to identify the user and apply RLS or business logic. This post summarizes Supabase's JWT, session/refresh, and how to verify a JWT on the backend (NestJS, Express, FastAPI, …).
Goal: Understand the auth flow (login → JWT → sent with each request), verify JWTs safely on the server, and the principle of "secrets server-side only".
Features
Supabase Auth
- Providers: Email/password, OAuth (Google, GitHub, etc.), magic link. Configure in Dashboard → Authentication → Providers.
- Session: After login the client receives access_token (JWT) and refresh_token; the access_token is sent with each request (header
Authorization: Bearer <token>). - JWT: Contains claims (sub = user id, email, role, etc.); signed with the project JWT secret. PostgREST uses the JWT to set
auth.uid()in RLS; your own backend verifies with the same secret to get the user id.
Refresh
- Access token has a short lifetime (e.g. 1 hour); the refresh token is used to get a new access token without logging in again. The Supabase JS client refreshes automatically when calling
getSession()or when the token is about to expire (depending on config).
Workflow / process
- Configure Auth (Dashboard): Enable providers, redirect URL; (optional) customize JWT expiry.
- Client login:
supabase.auth.signInWithPassword()or OAuth → receive session (access_token, refresh_token). - Client sends request: Send header
Authorization: Bearer <access_token>to PostgREST or your backend API. - PostgREST: Verifies JWT (using project JWT secret), sets
request.jwt.claims; RLS usesauth.uid()= sub. - Your backend: Verify JWT with the secret (from env), extract sub (user id); do not log the token or send it outside.
Convention: JWT secret exists only on the server; the client keeps the access_token in memory (or secure storage) and sends it with requests; never send refresh_token or secret to third parties.
Sample code
Client: sign in and send a request
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (data?.session) {
// Call the API with the token
const token = data.session.access_token;
const res = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` },
});
}Backend (Node/Express): verify JWT with secret
const jwt = require('jsonwebtoken');
function getUserIdFromRequest(req) {
const authHeader = req.headers.authorization;
const token = authHeader?.replace(/^Bearer\s+/i, '');
if (!token) return null;
try {
const secret = process.env.SUPABASE_JWT_SECRET; // from Dashboard → Settings → API
const decoded = jwt.verify(token, secret);
return decoded.sub; // user id
} catch {
return null;
}
}
// Don't log the token or secret.Backend (NestJS): guard using Passport JWT
// Use @nestjs/passport passport-jwt; the strategy verifies with the same secret and extracts sub.
// The guard attaches the user id to the request; the controller uses request.user.id.Apply in your architecture
- Auth at edge / server: Authentication happens on the server (PostgREST or backend); the client only carries the token. Never expose JWT secret or service role key to the client.
- Secrets server-side only: JWT secret and service role key are read only from env on the server; never logged or returned in responses. The client only needs the anon key (public) and access token (short-lived).
- One identity for many channels: The same JWT is used for PostgREST (RLS) and your backend API; both rely on the same user id for consistent permissions.
Summary
Supabase Auth issues a JWT after login; the client sends a Bearer token; PostgREST and your backend verify it with the JWT secret. Next: Prisma connecting to Supabase and syncing schema.
Next: 06 — Prisma + Supabase
Further reading: Supabase Auth — JWT
