Files
supabase__server/docs/core-primitives.md
Tomás Pozo 661329bb9e docs: add SDK documentation and SKILL.md (#20)
* docs: add initial documentation and skills.md

* docs: apply formatting

* docs: update SKILL.md to resolve docs from package location and ship docs with npm

SKILL.md now instructs agents to find documentation in the installed
@supabase/server package (node_modules or repo root) instead of using
relative paths. Added docs/ and SKILL.md to package.json files array
so they ship with npm installs.

* docs: add missing HTTPException import in error-handling example

* docs: fix strictNullChecks issues, duplicate variables, and missing context in examples

- Add non-null assertions (!) after error guards where TS can't narrow
  destructured result tuples
- Split duplicate variable declarations into separate code blocks
- Add missing imports and show where variables like `auth` come from
- Keep { data, error } destructuring pattern consistent with SDK convention

* docs: reframe as runtime-agnostic and add env auto-injection details

- getting-started: replace Edge Function framing with runtime-neutral
  language, explain module worker pattern works across Deno/Bun/Workers,
  add Runtimes section covering all supported environments
- webhooks: replace Deno.env with process.env for portable examples
- environment-variables: add "Auto-injected in" column distinguishing
  Platform vs Local CLI, reframe section headers
- auth-modes: clean up example key values
- core-primitives: clarify "Integration with frameworks" wording
- types: simplify TSDoc for publishable/secret key descriptions

* docs: add SSR frameworks guide and update references

Add docs/ssr-frameworks.md covering the pattern for using core
primitives in Next.js, SvelteKit, Nuxt, and Remix — cookie extraction,
env bridging, JWKS caching, and a complete Next.js adapter example.

Replace the basic SSR example in core-primitives.md with a pointer
to the new dedicated doc. Add SSR row to SKILL.md routing table.

* docs: add disclaimer of new package

* docs: extend explanation on keys env vars

* docs: add platform-specific quick starts to SKILL.md

Split the single generic example into per-platform sections
(Edge Functions, Cloudflare Workers, Hono, SSR Frameworks) so
AI agents pick the correct import specifier for each runtime.
Adds npm: prefix to all Deno examples and a Deno column to the
entry points table. Also adds createSupabaseContext examples.

* docs: add server-to-server quick starts and allow:always guardrails

Add secret key auth and webhook signature verification quick starts
to SKILL.md. Add explicit decision tree for allow:'always' so AI
agents confirm with the user before leaving endpoints unprotected.

* docs: add legacy keys warning, skills install, remove webhook docs

- Add legacy keys warning to SKILL.md (avoid anon/service_role keys)
- Add AI coding skills install section to README
- Add server-to-server quick start with caller code to README
- Add runtimes, documentation table, and named secret keys to README
- Remove verifyWebhookSignature references from all docs
- Delete docs/webhooks.md (code removal in separate PR)

* docs: add verify_jwt = false note for non-user auth modes

Edge Functions require verify_jwt = false in config.toml when
using allow: public, secret, or always — otherwise the platform
rejects requests before the handler runs.

* docs: add edge function recipes and refactor env vars doc

Add recipes for function-to-function calls, pg_net from database,
Stripe webhooks, and generic webhook signature verification.
Document the @supabase/server/wrappers entry point.
Refactor environment-variables.md into Supabase vs non-Supabase sections.

* docs: add security doc covering timing-safe comparison, auth model, CORS

* docs: link auth-modes timing-safe mentions to security.md

* docs: adding 'local cli' to secrets table

This envs will be injected from cli too

* docs: setting Deno as first installation choice

* docs: adding 'verify_jwt=false' disclaimer for non-user auth

* docs: split Deno/Supabase runtime section, merge Deno/Node/Bun

* docs(skills): adding legacy code migration example

* docs(skills): explaining why legacy code should be migrated

* docs: rewrite migration section, improve skill description triggers

---------

Co-authored-by: Kalleby Santos <kalleby_santos@hotmail.com>
2026-03-31 16:14:55 -05:00

6.9 KiB

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 { authType, token, userClaims, claims, 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, {
  allow: 'user',
})

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

console.log(auth!.authType) // '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, {
  allow: ['user', 'public'],
})

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

// Wildcard
const { data: auth } = await verifyCredentials(creds, {
  allow: '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, {
  allow: '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, { allow: '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

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' },
})

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, { allow: '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, {
        allow: '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 })
  },
}

SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)

In SSR frameworks, the JWT lives in session cookies rather than the Authorization header. Use verifyCredentials with a token extracted from cookies, then create clients as usual. This is the key primitive that enables SSR integration — it accepts pre-extracted credentials from any source.

For a complete guide with cookie parsing, JWKS caching, env bridging, and full framework adapters, see ssr-frameworks.md.