https://github.com/supabase/server
Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.
https://github.com/supabase/server
Last synced: 3 months ago
JSON representation
Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.
- Host: GitHub
- URL: https://github.com/supabase/server
- Owner: supabase
- License: mit
- Created: 2026-02-24T15:12:29.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-04-22T14:12:21.000Z (3 months ago)
- Last Synced: 2026-04-22T16:15:41.838Z (3 months ago)
- Language: TypeScript
- Homepage:
- Size: 153 KB
- Stars: 14
- Watchers: 0
- Forks: 3
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: docs/security.md
Awesome Lists containing this project
README
# @supabase/server
[](./LICENSE)
[](https://www.npmjs.com/package/@supabase/server)
[](https://pkg.pr.new/~/supabase/server)
> **Beta:** This package is under active development. APIs and documentation may change. If you find a bug or have a feature request, please [open an issue](https://github.com/supabase/server/issues) or [submit a PR](https://github.com/supabase/server/blob/main/CONTRIBUTING.md).
`@supabase/server` gives you batteries included access to the
[supabase-js SDK](https://github.com/supabase/supabase-js), including client
creation and authentication automatically scoped to the inbound requests to your
Edge Functions and APIs.
```ts
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
// RLS-scoped — this user only sees their own favorites
const { data: myGames } = await ctx.supabase.from('favorite_games').select()
return Response.json(myGames)
}),
}
```
One import. One line of config. Auth is validated, clients are ready, CORS is handled. Your handler only runs on successful auth.
## Installation
```bash
# Deno / Supabase Edge Functions (no install — import directly)
import { withSupabase } from "npm:@supabase/server";
# npm
npm install @supabase/server
# pnpm
pnpm add @supabase/server
```
### AI coding skills
Install the skill so your AI coding agent (Claude Code, Cursor, etc.) knows how to use this package:
```bash
npx skills add supabase/server
```
## Quick Start
Imagine you're building an app where users track their favorite games. They sign in and manage their own list. An admin dashboard curates featured titles. A cron job refreshes the "popular this week" rankings. Here's how each piece looks:
### Authenticated endpoint
```ts
// A signed-in user fetches their favorite games.
export default {
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
const { supabase, supabaseAdmin, userClaims, claims, authType } = ctx
// supabase — RLS-scoped to the authenticated user
// supabaseAdmin — bypasses RLS (service role)
// userClaims — user identity from JWT (id, email, role)
// claims — full JWT claims
// authType — which auth mode matched
// RLS-scoped — this user only sees their own favorites
const { data: myGames } = await supabase.from('favorite_games').select()
return Response.json(myGames)
}),
}
```
### Public endpoint (no auth)
```ts
// The frontend hits this before showing the login screen.
// allow: 'always' means no credentials required.
export default {
fetch: withSupabase({ allow: 'always' }, async (_req, _ctx) => {
return Response.json({ status: 'ok' })
}),
}
```
### API key protected
```ts
// An admin dashboard fetches the list of featured games to curate.
// Secret key auth (not a user JWT) — supabaseAdmin bypasses RLS.
export default {
fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
const { data: featuredGames } = await ctx.supabaseAdmin
.from('featured_games')
.select()
return Response.json(featuredGames)
}),
}
```
### Dual auth (user or service)
```ts
// Users view their own play stats from the app (JWT).
// A backend service pulls stats for any user (secret key + user_id in body).
export default {
fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
const callerIsUser = ctx.authType === 'user'
if (callerIsUser) {
// RLS-scoped — the database enforces "own stats only"
const { data: myStats } = await ctx.supabase.from('play_stats').select()
return Response.json(myStats)
}
// Service path — bypass RLS to pull stats for any user
const { user_id } = await req.json()
const { data: playStats } = await ctx.supabaseAdmin
.from('play_stats')
.select()
.eq('user_id', user_id)
return Response.json(playStats)
}),
}
```
### Server-to-server
```ts
// A cron job refreshes the "popular this week" list every hour.
// Named key ("cron") so it can be rotated without touching other services.
export default {
fetch: withSupabase({ allow: 'secret:cron' }, async (_req, ctx) => {
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const { data: popularThisWeek } = await ctx.supabaseAdmin.rpc(
'get_most_favorited_since',
{ since: oneWeekAgo.toISOString(), limit_count: 10 },
)
await ctx.supabaseAdmin
.from('featured_games')
.upsert(
popularThisWeek.map((g) => ({ game_id: g.id, reason: 'popular' })),
)
return Response.json({ popularThisWeek })
}),
}
```
The cron job sends the named secret key in the `apikey` header:
```ts
const refreshEndpoint =
'https://.supabase.co/functions/v1/refresh-popular'
const cronKey = 'sb_secret_...' // the "cron" named secret key
await fetch(refreshEndpoint, {
method: 'POST',
headers: { apikey: cronKey },
})
```
## 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. An absent credential falls through to the next mode; a present-but-invalid JWT rejects the request (no silent downgrade). See [`docs/auth-modes.md`](docs/auth-modes.md).
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`:
>
> ```toml
> [functions.my-function]
> verify_jwt = false
> ```
## Context
Every handler receives a `SupabaseContext`:
```ts
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
authKeyName?: string | null // Auth key name of the API key that was used for this request
}
```
`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
```ts
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](https://supabase.com/docs/guides/functions/cors). Pass a `Record` to set custom headers, or `false` to disable CORS handling (e.g. when using a framework that handles CORS separately).
```ts
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
```ts
import { Hono } from 'hono'
import { withSupabase } from '@supabase/server/adapters/hono'
const app = new Hono()
// Protected — withSupabase middleware validates the JWT before the handler runs
app.get('/games', withSupabase({ allow: 'user' }), async (c) => {
const { supabase } = c.var.supabaseContext
const { data: myGames } = await supabase.from('favorite_games').select()
return c.json(myGames)
})
// Public — no middleware means no auth
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`.
```ts
import {
verifyAuth,
createContextClient,
createAdminClient,
} from '@supabase/server/core'
```
### verifyAuth
Extracts credentials from a Request and validates against the allow config.
```ts
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.
```ts
const credentials = { token: myToken, apikey: null }
const { data: auth, error } = await verifyCredentials(credentials, {
allow: 'user',
})
```
### createContextClient / createAdminClient
```ts
const userScopedClient = createContextClient(auth.token) // RLS applies as this user
const anonClient = createContextClient() // RLS applies as anon role
const adminClient = createAdminClient() // bypasses RLS entirely
```
### createSupabaseContext
Full context assembly from a Request — `verifyAuth` + client creation in one call.
```ts
const { data: ctx, error } = await createSupabaseContext(req, { allow: 'user' })
```
### resolveEnv
Resolves environment variables with optional overrides.
```ts
const { data: env, error } = resolveEnv({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
})
```
### Example: custom multi-route handler
The same games API and health check from the Hono example, built from primitives instead of a framework:
```ts
import { verifyAuth, createContextClient } from '@supabase/server/core'
export default {
fetch: async (req) => {
const url = new URL(req.url)
// Public — no auth needed
if (url.pathname === '/health') {
return Response.json({ status: 'ok' })
}
// Protected — verify the JWT, then create a user-scoped client
if (url.pathname === '/games') {
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
if (error)
return Response.json(
{ message: error.message },
{ status: error.status },
)
const userScopedClient = createContextClient(auth.token)
const { data: myGames } = await userScopedClient
.from('favorite_games')
.select()
return Response.json(myGames)
}
return new Response('Not found', { status: 404 })
},
}
```
## Environment Variables
Automatically available in Supabase Edge Functions:
| Variable | Format | Description |
| --------------------------- | ------------------------------------------------------------- | ------------------------------------- |
| `SUPABASE_URL` | `https://.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`](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](#hono) or [core primitives](#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`](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`](docs/getting-started.md) |
| What auth modes are available? Array syntax? Named keys? | [`docs/auth-modes.md`](docs/auth-modes.md) |
| How do I use this with Hono? | [`docs/hono-adapter.md`](docs/hono-adapter.md) |
| How do I use low-level primitives for custom flows? | [`docs/core-primitives.md`](docs/core-primitives.md) |
| How do environment variables work across runtimes? | [`docs/environment-variables.md`](docs/environment-variables.md) |
| How do I handle errors? What codes exist? | [`docs/error-handling.md`](docs/error-handling.md) |
| How do I get typed database queries? | [`docs/typescript-generics.md`](docs/typescript-generics.md) |
| How do I use this in Next.js, Nuxt, SvelteKit, or Remix? | [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) |
| What's the complete API surface? | [`docs/api-reference.md`](docs/api-reference.md) |
## Development
```bash
pnpm install
pnpm dev
```
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, commit conventions, and release process.
## License
MIT