Supabase Auth
Supabase Auth (GoTrue) AuthStrategy — verifies Supabase-issued JWTs via the project's JWKS and maps app_metadata.tenant_id → tenantId.
@voltro/plugin-auth-supabase integrates Supabase Auth (GoTrue): an AuthStrategy (conforming to @voltro/protocol) that verifies Supabase-issued JWTs — by default against the project's JWKS endpoint (asymmetric public-key verify, no secret), or via an opt-in HS256 shared secret (jwtSecret) for legacy / self-hosted deployments that sign symmetrically and expose no JWKS — maps a server-controlled metadata claim to the framework's tenantId, and composes with other strategies (your password cookie, an API key, another IdP). It's a thin specialization of the shared jwtBearerStrategy — it adds Supabase's defaults and performs no verification of its own. Verified claims (both the server-controlled app_metadata and the user-controlled user_metadata blobs) arrive on the Subject under metadata.claims.
Install
pnpm add @voltro/plugin-auth-supabaseWiring
Add supabaseStrategy(...) to auth.strategies in your api's app.config.ts. The built-in signed-cookie password strategy always runs first; your strategies run after it, in order, until one matcheds (or one faileds).
// app.config.ts
import { supabaseStrategy } from '@voltro/plugin-auth-supabase'
export default {
type: 'api' as const,
name: 'myApi',
store: 'postgres' as const,
auth: {
strategies: [
supabaseStrategy({
projectRef: 'abcdef', // builds the hosted JWKS URL
// OR self-hosted GoTrue — pass an explicit URL + issuer:
// jwksUrl: 'https://my-supabase.example.com/auth/v1/.well-known/jwks.json',
// issuer: 'https://my-supabase.example.com/auth/v1',
// OR legacy / self-hosted Supabase signing with the shared secret
// (HS256, no JWKS) — verify with the secret instead:
// jwtSecret: process.env.SUPABASE_JWT_SECRET!, // server-only credential
// tenantClaim: 'app_metadata.tenant_id', // default (dotted path)
// defaultTenantId: 'public', // fallback for single-tenant deployments
}),
],
},
}Pass projectRef (hosted), jwksUrl (self-hosted GoTrue), or jwtSecret (legacy / self-hosted HS256 shared secret — see below). For a hosted project, the JWKS URL defaults to https://<projectRef>.supabase.co/auth/v1/.well-known/jwks.json and the issuer to https://<projectRef>.supabase.co/auth/v1. audience defaults to 'authenticated' (Supabase's role for signed-in users).
Tenant resolution maps from tenantClaim — default the dotted path app_metadata.tenant_id (a server-controlled blob users can't forge), then defaultTenantId. Apps that route tenants via user_metadata set tenantClaim: 'user_metadata.org' (same dotted-path resolution). Supply tenantIdFromClaims(claims) to derive it (returning null is a failed verdict).
cookieName defaults to null (header-only) — most Voltro apps send the token as an Authorization: Bearer header. For server-rendered / @supabase/ssr apps that keep the session in a cookie, set cookieName: 'sb-<projectRef>-auth-token'. @supabase/ssr does NOT store a raw JWT there — it stores the whole GoTrue session as a JSON envelope, by default prefixed with base64- and base64url-encoded, and (for large sessions) split across sb-<projectRef>-auth-token.0, .1, … chunk cookies. The strategy unwraps all of that — URL-decode, strip the base64- prefix + base64url-decode, concatenate chunks in order — down to the inner access_token before verifying it. The Authorization: Bearer header is always a raw JWT and is unaffected.
Strategies live on your type:'api' app — a type:'web' app has no auth middleware, so putting auth.strategies in a web app.config.ts does nothing. The web app forwards the request cookie to the api (including from ctx.query in a loader); the api's strategies verify it.
Client side — this strategy only verifies; your browser must send the token. For HTTP that's the normal
Authorization: Bearerheader; for live subscriptions over the WebSocket (the usual cross-origin api), pass it tomount({ apis: { api: { headers: async () => ({ authorization: 'Bearer ' + token }) } } })— a browser can't set WS upgrade headers, so without this every subscription connects anonymous. See Client: getting the token to the api.
To accept your own password sessions AND Supabase during a migration, build the chain explicitly with composeAuthStrategies (see auth strategies):
import { composeAuthStrategies } from '@voltro/protocol'
import { voltroPasswordStrategy } from '@voltro/plugin-auth'
import { supabaseStrategy } from '@voltro/plugin-auth-supabase'
const resolve = composeAuthStrategies([
voltroPasswordStrategy(),
supabaseStrategy({ projectRef: process.env.SUPABASE_PROJECT_REF! }),
])Options
| Option | Default | Notes |
|---|---|---|
projectRef |
— | The <ref> in <ref>.supabase.co. Required unless jwksUrl is set. |
jwksUrl |
https://<projectRef>.supabase.co/auth/v1/.well-known/jwks.json |
For self-hosted GoTrue; takes precedence over projectRef. |
jwtSecret |
— | Opt-in HS256 mode for legacy / self-hosted Supabase that signs with the shared JWT_SECRET and exposes no JWKS. Tokens are verified symmetrically (HS256); the JWKS path is not used. Server-only credential — never ship it to the browser. |
issuer |
https://<projectRef>.supabase.co/auth/v1 |
Set explicitly for self-hosted. |
audience |
'authenticated' |
String or array. |
cookieName |
null (header-only) |
Set to sb-<ref>-auth-token for @supabase/ssr cookie-mode apps — the JSON / base64- / chunked session envelope is unwrapped to the inner access_token. |
tenantClaim |
'app_metadata.tenant_id' |
Dotted-path. |
tenantIdFromClaims |
(uses tenantClaim) |
Derive tenantId; null → failed. |
defaultTenantId |
(none) | Fallback for single-tenant deployments. |
scopesFromClaims |
(none) | Map claims (Supabase role, or an app_metadata permissions array) → Subject.scopes. |
Algorithms follow the mode — JWKS pins ['RS256', 'ES256'], jwtSecret pins ['HS256']; the two are mutually exclusive, so there is no alg-confusion downgrade surface (an RS256 setup can't be tricked into accepting an HS256-forged token). none is never accepted.
Environment variables
None read directly — pass projectRef / jwksUrl explicitly (typically from your own defineEnv-declared vars).
Security
Server-only; fails closed — a forged, expired, or wrong-audience token, or one with no resolvable tenant, yields failed, never matched. JWKS mode does public-key verify only and handles no secret. The opt-in jwtSecret mode (legacy / self-hosted) verifies HS256 with the project's shared secret — a server-side credential; keep it out of the browser bundle and source control (load it from your env). Prefer JWKS whenever the deployment offers it.