supabase-agent-skills
Agent Skills to help developers using AI agents with Supabase. Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently.
Coming from a 0.x release? See MIGRATION.md for the v0 → v1 rename map (allow → auth, 'public' → 'publishable', authType → authMode, claims → jwtClaims, …).
$ npx -y skills add supabase/server --agent claude-code
Coming from a
0.xrelease? See MIGRATION.md for the v0 → v1 rename map (allow→auth,'public'→'publishable',authType→authMode,claims→jwtClaims, …).
@supabase/server gives you batteries included access to the
supabase-js SDK, including client
creation and authentication automatically scoped to the inbound requests to your
Edge Functions and APIs.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ auth: '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.
# Deno / Supabase Edge Functions (no install — import directly)
import { withSupabase } from "npm:@supabase/server@1";
# npm
npm install @supabase/server
# pnpm
pnpm add @supabase/server
Install the skill so your AI coding agent (Claude Code, Cursor, etc.) knows how to use this package:
npx skills add supabase/server
Imagine you're building an app where users track their favorite games. They sign in and manage their own list. Pre-login screens browse the public catalog. An admin dashboard curates featured titles. A cron job refreshes the "popular this week" rankings. Here's how each piece looks:
// A signed-in user fetches their favorite games.
export default {
fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
const { supabase, supabaseAdmin, userClaims, jwtClaims, authMode } = ctx
// supabase — RLS-scoped to the authenticated user
// supabaseAdmin — bypasses RLS (service role)
// userClaims — user identity from JWT (id, email, role)
// jwtClaims — full JWT claims
// authMode — 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)
}),
}
// The frontend hits this before showing the login screen.
// auth: 'none' means no credentials required.
export default {
fetch: withSupabase({ auth: 'none' }, async (_req, _ctx) => {
return Response.json({ status: 'ok' })
}),
}
// The mobile app browses the game catalog before the user signs in.
// auth: 'publishable' validates the apikey header against the 'default' publishable key —
// gating the endpoint to your own clients while staying anonymous to the DB.
export default {
fetch: withSupabase({ auth: 'publishable' }, async (_req, ctx) => {
// ctx.supabase — anonymous (anon role); RLS still applies
// ctx.userClaims, ctx.jwtClaims — null (no JWT)
// ctx.authMode === 'publishable', ctx.authKeyName === 'default'
const { data: catalog } = await ctx.supabase
.from('games')
.select('id, name, cover_url')
return Response.json(catalog)
}),
}
The mobile app sends the publishable key in the apikey header:
const catalogEndpoint = 'https://<project>.supabase.co/functions/v1/catalog'
const publishableKey = 'sb_publishable_...'
await fetch(catalogEndpoint, { headers: { apikey: publishableKey } })
Unlike
auth: 'secret', thesupabaseclient here is anonymous, not admin — RLS is the source of truth for what's visible. The publishable key acts as a coarse "this request came from a known client" gate; it isn't a user identity.
// An admin dashboard fetches the list of featured games to curate.
// auth: 'secret' validates the apikey header against the 'default' secret key
// (not a user JWT) — supabaseAdmin bypasses RLS.
export default {
fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => {
const { data: featuredGames } = await ctx.supabaseAdmin
.from('featured_games')
.select()
return Response.json(featuredGames)
}),
}
// 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({ auth: ['user', 'secret'] }, async (req, ctx) => {
const callerIsUser = ctx.authMode === '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)
}),
}
// 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({ auth: '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:
const refreshEndpoint =
'https://<project>.supabase.co/functions/v1/refresh-popular'
const cronKey = 'sb_secret_...' // the "cron" named secret key
await fetch(refreshEndpoint, {
method: 'POST',
headers: { apikey: cronKey },
})
| Mode | Credential | Use case |
|---|---|---|
"user" (default) | Valid JWT | Authenticated user endpoints |
"publishable" | Valid default publishable key | Client-facing, key-validated endpoints |
"secret" | Valid default secret key | Server-to-server, internal calls |
"none" | None | Open endpoints, wrappers that handle their own auth |
Array syntax (auth: ["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). Because "none" matches every request, it is only accepted on its own or as the last entry of a list (auth: ["user", "none"] — optional user).
Named key validation: auth: "publishable:web_app" or auth: "secret:automations" validates against a specific named key in SUPABASE_PUBLISHABLE_KEYS or SUPABASE_SECRET_KEYS. Bare auth: "secret" (or "publishable") matches only the default key; use the wildcard auth: "secret:*" to accept any key in the set. See docs/auth-modes.md.
Supabase Edge Functions: By default, the platform requires a valid JWT on every request. If your function uses
auth: 'publishable',auth: 'secret', orauth: 'none', disable the platform-level JWT check insupabase/config.toml:[functions.my-function] verify_jwt = false
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())
jwtClaims: JWTClaims | null // Present when auth is JWT
authMode: AuthMode // Which auth mode matched
authKeyName?: string // Auth key name of the API key that was used for this request (omitted for `'user'` / `'none'`)
}
supabase is always the safe client — it respects RLS. When authMode 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.
withSupabase(
{
auth: 'user', // who can call this function
cors: 'disabled', // disable CORS (default: supabase-js CORS headers)
env: { url: '...' }, // env overrides (optional)
},
handler,
)
cors accepts 'default' (the standard supabase-js CORS headers, also the default), 'disabled' to disable CORS handling (e.g. when using a framework that handles CORS separately), or { headers } to set custom headers. The boolean (true/false) and bare Record<string, string> forms are deprecated but still accepted.
withSupabase(
{
auth: 'user',
cors: {
headers: {
'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.
audience and issuer pin the aud and iss claims of a user-mode token. Each takes a string or an array. A token that lacks the claim, or carries a value outside the list, is rejected with INVALID_JWT. Supabase Auth sets aud to authenticated and iss to https://<project-ref>.supabase.co/auth/v1, so issuer: fromSupabaseUrl(url) limits an endpoint to tokens from one project.
import { fromSupabaseUrl, withSupabase } from '@supabase/server'
withSupabase(
{
auth: 'user',
issuer: fromSupabaseUrl('https://<project-ref>.supabase.co'),
},
handler,
)
Called with config only, withSupabase(config) returns an entry for pipeline from @supabase/middleware. Entries placed after it in the array receive the Supabase context and contribute their own typed keys; entries placed before it run ahead of the auth gate. The first-party entries live on the @supabase/server/middleware/* subpaths; see Postgres.
Adapters wrap withSupabase for a specific framework's middleware contract. They ship inside @supabase/server, so a single npm install @supabase/server covers the framework you're using — no separate package per adapter.
[!WARNING] Adapters are deprecated and will be removed on December 1, 2026. Use middleware to integrate
@supabase/serverwith your framework.We're no longer accepting new adapter submissions.
For Hono, H3, Elysia, NestJS, or TanStack Start, use the framework bridges in
examples/frameworks. Read the framework integration guide for setup steps.
| Framework | Import | Framework version | Docs |
Agent Skills to help developers using AI agents with Supabase. Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently.
Supabase is the Postgres development platform. We're building the features of Firebase using enterprise-grade open source tools. [x] Hosted Postgres Database. Docs [x] Authentication and Authorization. Docs [x] Auto-generated APIs. [x] REST. Docs [x] GraphQL.
server is a Claude Code plugin with 1 hand-picked skill, indexed on Flowy in the Backend category. Install it with the command on its page. It includes supabase-server. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it