Skip to content

Latest commit

 

History

History
244 lines (178 loc) · 7.77 KB

File metadata and controls

244 lines (178 loc) · 7.77 KB

Core Primitives

When to use primitives

Use withSupabase or createSupabaseContext for standard use cases. Drop down to core primitives when you need:

  • Multiple routes with different auth in a single handler
  • Custom response headers or error formats
  • Integration with frameworks other than the ones provided
  • Pre-extracted credentials (e.g., from cookies, custom headers)
  • Just auth verification without client creation

All primitives are available from @supabase/server/core.

The composition pipeline

The primitives compose into a pipeline. Each step is independent — use only what you need:

resolveEnv()                          → SupabaseEnv
extractCredentials(request)           → Credentials { token, apikey }
verifyCredentials(credentials, opts)  → AuthResult { authMode, token, userClaims, jwtClaims, keyName }
createContextClient(options)          → SupabaseClient (RLS-scoped)
createAdminClient(options)            → SupabaseClient (bypasses RLS)

Or use the convenience function that combines extraction and verification:

verifyAuth(request, opts)  → AuthResult (extractCredentials + verifyCredentials in one call)

resolveEnv

Resolves Supabase environment configuration from runtime variables. The only hard requirement is SUPABASE_URL.

import { resolveEnv } from '@supabase/server/core'

const { data: env, error } = resolveEnv()
if (error) {
  // error is an EnvError — e.g., SUPABASE_URL not set
  console.error(error.message)
}

With partial overrides:

const { data: envOverridden } = resolveEnv({
  url: 'http://localhost:54321',
})

Returns { data: SupabaseEnv, error: null } on success, { data: null, error: EnvError } on failure.

extractCredentials

Pure extraction — reads headers, performs no validation.

import { extractCredentials } from '@supabase/server/core'

const creds = extractCredentials(request)
// creds.token  → string | null  (from Authorization: Bearer <token>)
// creds.apikey → string | null  (from apikey header)

This is synchronous and never fails. Fields are null when the corresponding header is absent.

verifyCredentials

Verifies pre-extracted credentials against allowed auth modes. Use this when credentials come from a non-standard source (cookies, custom headers, etc.).

import { verifyCredentials } from '@supabase/server/core'

const credentials = { token: cookieToken, apikey: null }
const { data: auth, error } = await verifyCredentials(credentials, {
  auth: 'user',
})

if (error) {
  return Response.json({ message: error.message }, { status: error.status })
}

console.log(auth!.authMode) // 'user'
console.log(auth!.userClaims) // { id: '...', email: '...', role: 'authenticated' }

Supports all auth mode syntax — single mode, arrays, and named keys:

// Multiple modes
const { data: auth } = await verifyCredentials(creds, {
  auth: ['user', 'publishable'],
})

// Named key
const { data: auth } = await verifyCredentials(creds, {
  auth: 'publishable:web',
})

// Wildcard
const { data: auth } = await verifyCredentials(creds, {
  auth: 'secret:*',
})

verifyAuth

Convenience function that combines extractCredentials and verifyCredentials in a single call. Use this when working with a standard Request:

import { verifyAuth } from '@supabase/server/core'

const { data: auth, error } = await verifyAuth(request, {
  auth: 'user',
})

if (error) {
  return Response.json({ message: error.message }, { status: error.status })
}

console.log(auth.userClaims!.id) // "d0f1a2b3-..."
console.log(auth.token) // the verified JWT string

createContextClient

Creates a Supabase client scoped to the caller's identity. RLS policies apply.

import { verifyAuth, createContextClient } from '@supabase/server/core'

// With a user's token (from verifyAuth)
const { data: auth } = await verifyAuth(request, { auth: 'user' })
const supabase = createContextClient({
  auth: { token: auth!.token, keyName: auth!.keyName },
})
// Anonymous (no token) — RLS as anon role
const anonClient = createContextClient()

The client is configured with:

  • The publishable key as the apikey header
  • The user's JWT as the Authorization: Bearer header (if token is provided)
  • Server-safe auth settings: persistSession: false, autoRefreshToken: false, detectSessionInUrl: false

Which key is used. With auth.keyName set, that named key from SUPABASE_PUBLISHABLE_KEYS is used (and it throws if the key doesn't exist). With keyName omitted, the default key is used, falling back to the first key in the set when no default exists. Note this differs from the 'publishable' auth mode, which matches the default key only and never falls back.

This function throws EnvError if SUPABASE_URL or the required publishable key is missing. Wrap in try/catch when using directly.

createAdminClient

Creates a Supabase client that bypasses Row-Level Security using a secret key.

import { createAdminClient } from '@supabase/server/core'

const supabaseAdmin = createAdminClient()
// With a specific named key
const supabaseAdminInternal = createAdminClient({
  auth: { keyName: 'internal' },
})

Which key is used. With auth.keyName set, that named key from SUPABASE_SECRET_KEYS is used (and it throws if the key doesn't exist). With keyName omitted, the default key is used, falling back to the first key in the set when no default exists. Note this differs from the 'secret' auth mode, which matches the default key only and never falls back.

Same server-safe settings as createContextClient. Throws EnvError if the secret key is missing.

Full example: custom multi-route handler

Using primitives to build a handler with different auth per route, without a framework:

import {
  verifyAuth,
  createContextClient,
  createAdminClient,
} from '@supabase/server/core'

export default {
  fetch: async (req: Request) => {
    const url = new URL(req.url)

    // Public route — no auth needed
    if (url.pathname === '/health') {
      return Response.json({ status: 'ok' })
    }

    // User-authenticated route
    if (url.pathname === '/todos') {
      const { data: auth, error } = await verifyAuth(req, { auth: 'user' })
      if (error) {
        return Response.json(
          { message: error.message },
          { status: error.status },
        )
      }

      const supabase = createContextClient({
        auth: { token: auth!.token, keyName: auth!.keyName },
      })
      const { data } = await supabase.from('todos').select()
      return Response.json(data)
    }

    // Admin route — secret key only
    if (url.pathname === '/admin/users') {
      const { data: auth, error } = await verifyAuth(req, {
        auth: 'secret',
      })
      if (error) {
        return Response.json(
          { message: error.message },
          { status: error.status },
        )
      }

      const supabaseAdmin = createAdminClient({
        auth: { keyName: auth!.keyName },
      })
      const { data } = await supabaseAdmin.from('profiles').select()
      return Response.json(data)
    }

    return new Response('Not found', { status: 404 })
  },
}

Cookie-based environments (with @supabase/ssr)

In Next.js, SvelteKit, Remix, and other cookie-based frameworks, the JWT lives in session cookies rather than the Authorization header. The recommended pattern is to compose with @supabase/ssr: let @supabase/ssr own the cookie session lifecycle and refresh-token rotation (via middleware), then hand its fresh access token to verifyCredentials and build typed clients with createContextClient + createAdminClient.

For the full pattern — middleware setup, the composed adapter, JWKS caching, and other-framework adapting tips — see ssr-frameworks.md.