Skip to content

Commit b87ddf4

Browse files
docs: grounded README examples around a cohesive "favorite games" app (#28)
* docs: rewrite README examples around a cohesive "favorite games" scenario * docs: update quick start cron job description to reflect popular rankings use case * docs: split user/service auth paths into explicit branches for clarity * docs: rewrite README intro to clarify SDK relationship and request scoping * docs: put deno installation as first option --------- Co-authored-by: Kalleby Santos <kalleby_santos@hotmail.com>
1 parent b87e89a commit b87ddf4

2 files changed

Lines changed: 89 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,9 @@
3131

3232
## [0.1.2](https://github.com/supabase/server/compare/server-v0.1.1...server-v0.1.2) (2026-04-01)
3333

34-
3534
### Features
3635

37-
* exposing `keyName` to `SupabaseContext` ([#22](https://github.com/supabase/server/issues/22)) ([7f1b1a7](https://github.com/supabase/server/commit/7f1b1a75cc98d08a63275131481e5df825c10afb))
36+
- exposing `keyName` to `SupabaseContext` ([#22](https://github.com/supabase/server/issues/22)) ([7f1b1a7](https://github.com/supabase/server/commit/7f1b1a75cc98d08a63275131481e5df825c10afb))
3837

3938
## [0.1.1](https://github.com/supabase/server/compare/server-v0.1.0...server-v0.1.1) (2026-03-26)
4039

README.md

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,36 @@
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
1013
import { withSupabase } from '@supabase/server'
1114

1215
export 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
2633
npm install @supabase/server
2734

2835
# pnpm
2936
pnpm 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.
4855
export 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.
6576
export 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.
7588
export 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).
86103
export 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.
102129
export 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
202235
const 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
210245
app.get('/health', (c) => c.json({ status: 'ok' }))
211246
212247
export 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
282319
import { verifyAuth, createContextClient } from '@supabase/server/core'
283320
284321
export 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

Comments
 (0)