Files
supabase__server/README.md
T
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

13 KiB

@supabase/server

License Package pkg.pr.new

Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.

import { withSupabase } from '@supabase/server'

export default {
  fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

One import. One line of config. Auth is validated, clients are scoped, CORS is handled. Your handler only runs on successful auth.

Installation

# npm
npm install @supabase/server

# pnpm
pnpm add @supabase/server

# Deno / Supabase Edge Functions (no install — import directly)
import { withSupabase } from "npm:@supabase/server";

AI coding skills

Install the skill so your AI coding agent (Claude Code, Cursor, etc.) knows how to use this package:

npx skills add supabase/server

Quick Start

Authenticated endpoint

export default {
  fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
    // ctx.supabase — RLS-scoped to the authenticated user
    // ctx.supabaseAdmin — bypasses RLS (service role)
    // ctx.userClaims — user identity from JWT (id, email, role)
    // ctx.claims — JWT claims
    // ctx.authType — which auth mode matched

    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

Public endpoint (no auth)

export default {
  fetch: withSupabase({ allow: 'always' }, async (_req, _ctx) => {
    return Response.json({ status: 'ok' })
  }),
}

API key protected

export default {
  fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
    const { data } = await ctx.supabaseAdmin.from('config').select()
    return Response.json(data)
  }),
}

Dual auth (user or service)

export default {
  fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
    const userId = ctx.userClaims?.id ?? (await req.json()).user_id
    const { data } = await ctx.supabaseAdmin
      .from('reports')
      .select()
      .eq('user_id', userId)
    return Response.json(data)
  }),
}

Server-to-server

// Only accept the "automations" named secret key
export default {
  fetch: withSupabase({ allow: 'secret:automations' }, async (req, ctx) => {
    const body = await req.json()
    const { data } = await ctx.supabaseAdmin
      .from('scheduled_tasks')
      .insert({ name: body.taskName })
    return Response.json({ success: true, data })
  }),
}

The caller sends the secret key in the apikey header:

await fetch('https://<project>.supabase.co/functions/v1/my-function', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    apikey: 'sb_secret_...', // the "automations" secret key
  },
  body: JSON.stringify({ taskName: 'cleanup' }),
})

Auth Modes

Mode Credential Use case
"user" (default) Valid JWT Authenticated user endpoints
"public" Valid publishable key Client-facing, key-validated endpoints
"secret" Valid secret key Server-to-server, internal calls
"always" None Open endpoints, wrappers that handle their own auth

Array syntax (allow: ["user", "secret"]) accepts multiple auth methods — first match wins.

Named key validation: allow: "public:web_app" or allow: "secret:automations" validates against a specific named key in SUPABASE_PUBLISHABLE_KEYS or SUPABASE_SECRET_KEYS.

Supabase Edge Functions: By default, the platform requires a valid JWT on every request. If your function uses allow: 'public', allow: 'secret', or allow: 'always', disable the platform-level JWT check in supabase/config.toml:

[functions.my-function]
verify_jwt = false

Context

Every handler receives a SupabaseContext:

interface SupabaseContext {
  supabase: SupabaseClient // RLS-scoped (user or anon depending on auth)
  supabaseAdmin: SupabaseClient // Bypasses RLS
  userClaims: UserClaims | null // JWT-derived identity (for full User, call supabase.auth.getUser())
  claims: JWTClaims | null // Present when auth is JWT
  authType: Allow // Which auth mode matched
}

supabase is always the safe client — it respects RLS. When authType is "user", it's scoped to that user's permissions. Otherwise, it's initialized as anonymous.

supabaseAdmin always bypasses RLS. Use it for operations that need full database access.

Config

withSupabase(
  {
    allow: 'user', // who can call this function
    cors: false, // disable CORS (default: supabase-js CORS headers)
    env: { url: '...' }, // env overrides (optional)
  },
  handler,
)

cors defaults to the standard supabase-js CORS headers. Pass a Record<string, string> to set custom headers, or false to disable CORS handling (e.g. when using a framework that handles CORS separately).

withSupabase(
  {
    allow: 'user',
    cors: {
      'Access-Control-Allow-Origin': 'https://myapp.com',
      'Access-Control-Allow-Headers': 'authorization, content-type',
    },
  },
  handler,
)

env overrides environment variable resolution. Defaults to reading SUPABASE_URL, SUPABASE_PUBLISHABLE_KEYS, SUPABASE_SECRET_KEYS, and SUPABASE_JWKS from the runtime environment.

Framework Adapters

Hono

import { Hono } from 'hono'
import { withSupabase } from '@supabase/server/adapters/hono'

const app = new Hono()

app.get('/todos', withSupabase({ allow: 'user' }), async (c) => {
  const { supabase: sb } = c.var.supabaseContext
  const { data } = await sb.from('todos').select()
  return c.json(data)
})

app.get('/health', (c) => c.json({ status: 'ok' }))

export default { fetch: app.fetch }

The adapter does not handle CORS — use hono/cors for that. Per-route auth works naturally by applying the middleware to specific routes.

Primitives

For when you need more control than withSupabase provides — multiple routes with different auth, custom response headers, or building your own wrapper.

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

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

verifyAuth

Extracts credentials from a Request and validates against the allow config.

const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
if (error) {
  return Response.json({ message: error.message }, { status: error.status })
}

verifyCredentials

Low-level — works with raw credentials instead of a Request. Used by SSR adapters and custom auth flows.

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

createContextClient / createAdminClient

const supabase = createContextClient(auth.token) // user-scoped, RLS applies
const supabase = createContextClient() // anonymous, RLS as anon
const supabaseAdmin = createAdminClient() // bypasses RLS

createSupabaseContext

Full context assembly from a Request — verifyAuth + client creation in one call.

const { data: ctx, error } = await createSupabaseContext(req, { allow: 'user' })

resolveEnv

Resolves environment variables with optional overrides.

const { data: env, error } = resolveEnv({
  url: process.env.NEXT_PUBLIC_SUPABASE_URL,
})

Example: custom multi-route handler

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

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

    if (url.pathname === '/health') {
      return Response.json({ status: 'ok' })
    }

    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)
      const { data } = await supabase.from('todos').select()
      return Response.json(data)
    }

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

Environment Variables

Automatically available in Supabase Edge Functions:

Variable Format Description
SUPABASE_URL https://<ref>.supabase.co Your project URL
SUPABASE_PUBLISHABLE_KEYS {"default":"sb_publishable_...","web":"sb_publishable_..."} Publishable API keys (named)
SUPABASE_SECRET_KEYS {"default":"sb_secret_...","web":"sb_secret_..."} Secret API keys (named)
SUPABASE_JWKS {"keys":[...]} or [...] JSON Web Key Set for JWT verification

Also supported (for local dev, self-hosted, or other runtimes):

Variable Format Description
SUPABASE_PUBLISHABLE_KEY sb_publishable_... Single publishable key
SUPABASE_SECRET_KEY sb_secret_... Single secret key

When both singular and plural forms are set, plural takes priority.

For other environments, pass overrides via the env config option or resolveEnv(). See docs/environment-variables.md for details.

Runtimes

  • Supabase Edge Functions — environment variables are auto-injected. Zero config.
  • Deno / Bun — works out of the box with the export default { fetch } pattern.
  • Node.js — use the Hono adapter or core primitives with your framework of choice.
  • Cloudflare Workers — enable nodejs_compat in wrangler.toml or pass env overrides via the env config option.
  • Next.js / Nuxt / SvelteKit / Remix — use core primitives to build a cookie-based auth adapter. See docs/ssr-frameworks.md.

Exports

Export What's in it
@supabase/server withSupabase, createSupabaseContext
@supabase/server/core verifyAuth, verifyCredentials, extractCredentials, createContextClient, createAdminClient, resolveEnv
@supabase/server/adapters/hono withSupabase (Hono middleware)

Documentation

Question Doc file
How do I create a basic endpoint? docs/getting-started.md
What auth modes are available? Array syntax? Named keys? docs/auth-modes.md
How do I use this with Hono? docs/hono-adapter.md
How do I use low-level primitives for custom flows? docs/core-primitives.md
How do environment variables work across runtimes? docs/environment-variables.md
How do I handle errors? What codes exist? docs/error-handling.md
How do I get typed database queries? docs/typescript-generics.md
How do I use this in Next.js, Nuxt, SvelteKit, or Remix? docs/ssr-frameworks.md
What's the complete API surface? docs/api-reference.md

Development

pnpm install
pnpm dev

Contributing

See CONTRIBUTING.md for development workflow, commit conventions, and release process.

License

MIT