44[ ![ Package] ( https://img.shields.io/npm/v/@supabase/server )] ( https://www.npmjs.com/package/@supabase/server )
55[ ![ pkg.pr.new] ( https://pkg.pr.new/badge/supabase/server )] ( https://pkg.pr.new/~/supabase/server )
66
7- Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.
7+ ` @supabase/server ` gives you batteries included access to the
8+ [ supabase-js SDK] ( https://github.com/supabase/supabase-js ) , including client
9+ creation and authentication automatically scoped to the inbound requests to your
10+ Edge Functions and APIs.
811
912``` ts
1013import { withSupabase } from ' @supabase/server'
1114
1215export default {
1316 fetch: withSupabase ({ allow: ' user' }, async (_req , ctx ) => {
14- const { data } = await ctx .supabase .from (' todos' ).select ()
15- return Response .json (data )
17+ // RLS-scoped — this user only sees their own favorites
18+ const { data : myGames } = await ctx .supabase .from (' favorite_games' ).select ()
19+ return Response .json (myGames )
1620 }),
1721}
1822```
1923
20- One import. One line of config. Auth is validated, clients are scoped , CORS is handled. Your handler only runs on successful auth.
24+ One import. One line of config. Auth is validated, clients are ready , CORS is handled. Your handler only runs on successful auth.
2125
2226## Installation
2327
2428``` bash
29+ # Deno / Supabase Edge Functions (no install — import directly)
30+ import { withSupabase } from " npm:@supabase/server" ;
31+
2532# npm
2633npm install @supabase/server
2734
2835# pnpm
2936pnpm add @supabase/server
30-
31- # Deno / Supabase Edge Functions (no install — import directly)
32- import { withSupabase } from " npm:@supabase/server" ;
3337` ` `
3438
3539# ## AI coding skills
@@ -42,26 +46,33 @@ npx skills add supabase/server
4246
4347# # Quick Start
4448
49+ 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:
50+
4551# ## Authenticated endpoint
4652
4753` ` ` ts
54+ // A signed-in user fetches their favorite games.
4855export default {
4956 fetch: withSupabase({ allow: ' user' }, async (_req, ctx) => {
50- // ctx.supabase — RLS-scoped to the authenticated user
51- // ctx.supabaseAdmin — bypasses RLS (service role)
52- // ctx.userClaims — user identity from JWT (id, email, role)
53- // ctx.claims — JWT claims
54- // ctx.authType — which auth mode matched
55-
56- const { data } = await ctx.supabase.from('todos').select ()
57- return Response.json(data)
57+ const { supabase, supabaseAdmin, userClaims, claims, authType } = ctx
58+ // supabase — RLS-scoped to the authenticated user
59+ // supabaseAdmin — bypasses RLS (service role)
60+ // userClaims — user identity from JWT (id, email, role)
61+ // claims — full JWT claims
62+ // authType — which auth mode matched
63+
64+ // RLS-scoped — this user only sees their own favorites
65+ const { data: myGames } = await supabase.from('favorite_games').select ()
66+ return Response.json(myGames)
5867 }),
5968}
6069` ` `
6170
6271# ## Public endpoint (no auth)
6372
6473` ` ` ts
74+ // The frontend hits this before showing the login screen.
75+ // allow: ' always' means no credentials required.
6576export default {
6677 fetch: withSupabase({ allow: ' always' }, async (_req, _ctx) => {
6778 return Response.json({ status: ' ok' })
@@ -72,54 +83,76 @@ export default {
7283# ## API key protected
7384
7485` ` ` ts
86+ // An admin dashboard fetches the list of featured games to curate.
87+ // Secret key auth (not a user JWT) — supabaseAdmin bypasses RLS.
7588export default {
7689 fetch: withSupabase({ allow: ' secret' }, async (_req, ctx) => {
77- const { data } = await ctx.supabaseAdmin.from('config').select ()
78- return Response.json(data)
90+ const { data: featuredGames } = await ctx.supabaseAdmin
91+ .from(' featured_games' )
92+ .select ()
93+ return Response.json(featuredGames)
7994 }),
8095}
8196` ` `
8297
8398# ## Dual auth (user or service)
8499
85100` ` ` ts
101+ // Users view their own play stats from the app (JWT).
102+ // A backend service pulls stats for any user (secret key + user_id in body).
86103export default {
87104 fetch: withSupabase({ allow: [' user' , ' secret' ] }, async (req, ctx) => {
88- const userId = ctx.userClaims? .id ?? (await req.json ()).user_id
89- const { data } = await ctx.supabaseAdmin
90- .from(' reports' )
105+ const callerIsUser = ctx.authType === ' user'
106+
107+ if (callerIsUser) {
108+ // RLS-scoped — the database enforces " own stats only"
109+ const { data: myStats } = await ctx.supabase.from('play_stats').select ()
110+ return Response.json(myStats)
111+ }
112+
113+ // Service path — bypass RLS to pull stats for any user
114+ const { user_id } = await req.json ()
115+ const { data: playStats } = await ctx.supabaseAdmin
116+ .from(' play_stats' )
91117 .select ()
92- .eq(' user_id' , userId )
93- return Response.json(data )
118+ .eq(' user_id' , user_id )
119+ return Response.json(playStats )
94120 }),
95121}
96122` ` `
97123
98124# ## Server-to-server
99125
100126` ` ` ts
101- // Only accept the " automations" named secret key
127+ // A cron job refreshes the " popular this week" list every hour.
128+ // Named key (" cron" ) so it can be rotated without touching other services.
102129export default {
103- fetch: withSupabase({ allow: ' secret:automations' }, async (req, ctx) => {
104- const body = await req.json ()
105- const { data } = await ctx.supabaseAdmin
106- .from(' scheduled_tasks' )
107- .insert({ name: body.taskName })
108- return Response.json({ success: true, data })
130+ fetch: withSupabase({ allow: ' secret:cron' }, async (_req, ctx) => {
131+ const oneWeekAgo = new Date(Date.now () - 7 * 24 * 60 * 60 * 1000)
132+ const { data: popularThisWeek } = await ctx.supabaseAdmin.rpc(
133+ ' get_most_favorited_since' ,
134+ { since: oneWeekAgo.toISOString (), limit_count: 10 },
135+ )
136+ await ctx.supabaseAdmin
137+ .from(' featured_games' )
138+ .upsert(
139+ popularThisWeek.map(( g) => ({ game_id: g.id, reason: 'popular' })) ,
140+ )
141+ return Response.json({ popularThisWeek })
109142 }),
110143}
111144` ` `
112145
113- The caller sends the secret key in the ` apikey` header:
146+ The cron job sends the named secret key in the ` apikey` header:
114147
115148` ` ` ts
116- await fetch(' https://<project>.supabase.co/functions/v1/my-function' , {
149+ const refreshEndpoint =
150+ ' https://<project>.supabase.co/functions/v1/refresh-popular'
151+ const cronKey = ' sb_secret_...' // the " cron" named secret key
152+
153+ await fetch(refreshEndpoint, {
117154 method: ' POST' ,
118- headers: {
119- ' Content-Type' : ' application/json' ,
120- apikey: ' sb_secret_...' , // the " automations" secret key
121- },
122- body: JSON.stringify({ taskName: ' cleanup' }),
155+ headers: { apikey: cronKey },
123156})
124157` ` `
125158
@@ -201,12 +234,14 @@ import { withSupabase } from '@supabase/server/adapters/hono'
201234
202235const app = new Hono()
203236
204- app.get(' /todos' , withSupabase({ allow: ' user' }), async (c) => {
205- const { supabase: sb } = c.var.supabaseContext
206- const { data } = await sb.from(' todos' ).select()
207- return c.json(data)
237+ // Protected — withSupabase middleware validates the JWT before the handler runs
238+ app.get(' /games' , withSupabase({ allow: ' user' }), async (c) => {
239+ const { supabase } = c.var.supabaseContext
240+ const { data: myGames } = await supabase.from(' favorite_games' ).select()
241+ return c.json(myGames)
208242})
209243
244+ // Public — no middleware means no auth
210245app.get(' /health' , (c) => c.json({ status: ' ok' }))
211246
212247export default { fetch: app.fetch }
@@ -253,9 +288,9 @@ const { data: auth, error } = await verifyCredentials(credentials, {
253288### createContextClient / createAdminClient
254289
255290```ts
256- const supabase = createContextClient(auth.token) // user-scoped, RLS applies
257- const supabase = createContextClient() // anonymous, RLS as anon
258- const supabaseAdmin = createAdminClient() // bypasses RLS
291+ const userScopedClient = createContextClient(auth.token) // RLS applies as this user
292+ const anonClient = createContextClient() // RLS applies as anon role
293+ const adminClient = createAdminClient() // bypasses RLS entirely
259294```
260295
261296### createSupabaseContext
@@ -278,28 +313,34 @@ const { data: env, error } = resolveEnv({
278313
279314### Example: custom multi-route handler
280315
316+ The same games API and health check from the Hono example, built from primitives instead of a framework:
317+
281318```ts
282319import { verifyAuth, createContextClient } from ' @supabase/server/core'
283320
284321export default {
285322 fetch: async (req) => {
286323 const url = new URL(req.url)
287324
325+ // Public — no auth needed
288326 if (url.pathname === ' /health' ) {
289327 return Response.json({ status: ' ok' })
290328 }
291329
292- if (url.pathname === ' /todos' ) {
330+ // Protected — verify the JWT, then create a user-scoped client
331+ if (url.pathname === ' /games' ) {
293332 const { data: auth, error } = await verifyAuth(req, { allow: ' user' })
294333 if (error)
295334 return Response.json(
296335 { message: error.message },
297336 { status: error.status },
298337 )
299338
300- const supabase = createContextClient(auth.token)
301- const { data } = await supabase.from(' todos' ).select()
302- return Response.json(data)
339+ const userScopedClient = createContextClient(auth.token)
340+ const { data: myGames } = await userScopedClient
341+ .from(' favorite_games' )
342+ .select()
343+ return Response.json(myGames)
303344 }
304345
305346 return new Response(' Not found' , { status: 404 })
0 commit comments