@supabase/server 0.1.4 → 0.2.0-rc.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +146 -50
  2. package/dist/adapters/h3/index.cjs +59 -0
  3. package/dist/adapters/h3/index.d.cts +51 -0
  4. package/dist/adapters/h3/index.d.mts +51 -0
  5. package/dist/adapters/h3/index.mjs +58 -0
  6. package/dist/adapters/hono/index.cjs +1 -1
  7. package/dist/adapters/hono/index.d.cts +4 -4
  8. package/dist/adapters/hono/index.d.mts +4 -4
  9. package/dist/adapters/hono/index.mjs +1 -1
  10. package/dist/core/index.cjs +1 -1
  11. package/dist/core/index.d.cts +7 -4
  12. package/dist/core/index.d.mts +7 -4
  13. package/dist/core/index.mjs +1 -1
  14. package/dist/{create-supabase-context--VqMJpDu.cjs → create-supabase-context-C_8SbO5w.cjs} +1 -1
  15. package/dist/{create-supabase-context-B3Uzt_3I.mjs → create-supabase-context-DXD5rxi1.mjs} +1 -1
  16. package/dist/index.cjs +2 -2
  17. package/dist/index.d.cts +2 -2
  18. package/dist/index.d.mts +2 -2
  19. package/dist/index.mjs +2 -2
  20. package/dist/{types-CbC-wBUe.d.mts → types-DKe8uOwI.d.mts} +6 -1
  21. package/dist/{types-DxTr0Qum.d.cts → types-DqhOaSlC.d.cts} +6 -1
  22. package/dist/{verify-auth-DrgvEuKo.cjs → verify-auth-C4zqDlfj.cjs} +18 -5
  23. package/dist/{verify-auth-Bt2uGltH.mjs → verify-auth-CxFZy9rl.mjs} +18 -5
  24. package/docs/api-reference.md +11 -11
  25. package/docs/auth-modes.md +3 -1
  26. package/docs/error-handling.md +5 -5
  27. package/docs/security.md +3 -1
  28. package/package.json +12 -2
  29. package/skills/supabase-server/SKILL.md +5 -0
  30. /package/dist/{errors-CAH-RRA3.d.mts → errors-Dyj5Cjt6.d.cts} +0 -0
  31. /package/dist/{errors-O2ugIMec.d.cts → errors-m42mkqhD.d.mts} +0 -0
package/README.md CHANGED
@@ -4,32 +4,38 @@
4
4
  [![Package](https://img.shields.io/npm/v/@supabase/server)](https://www.npmjs.com/package/@supabase/server)
5
5
  [![pkg.pr.new](https://pkg.pr.new/badge/supabase/server)](https://pkg.pr.new/~/supabase/server)
6
6
 
7
- Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.
7
+ > **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).
8
+
9
+ `@supabase/server` gives you batteries included access to the
10
+ [supabase-js SDK](https://github.com/supabase/supabase-js), including client
11
+ creation and authentication automatically scoped to the inbound requests to your
12
+ Edge Functions and APIs.
8
13
 
9
14
  ```ts
10
15
  import { withSupabase } from '@supabase/server'
11
16
 
12
17
  export default {
13
18
  fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
14
- const { data } = await ctx.supabase.from('todos').select()
15
- return Response.json(data)
19
+ // RLS-scoped this user only sees their own favorites
20
+ const { data: myGames } = await ctx.supabase.from('favorite_games').select()
21
+ return Response.json(myGames)
16
22
  }),
17
23
  }
18
24
  ```
19
25
 
20
- One import. One line of config. Auth is validated, clients are scoped, CORS is handled. Your handler only runs on successful auth.
26
+ One import. One line of config. Auth is validated, clients are ready, CORS is handled. Your handler only runs on successful auth.
21
27
 
22
28
  ## Installation
23
29
 
24
30
  ```bash
31
+ # Deno / Supabase Edge Functions (no install — import directly)
32
+ import { withSupabase } from "npm:@supabase/server";
33
+
25
34
  # npm
26
35
  npm install @supabase/server
27
36
 
28
37
  # pnpm
29
38
  pnpm add @supabase/server
30
-
31
- # Deno / Supabase Edge Functions (no install — import directly)
32
- import { withSupabase } from "npm:@supabase/server";
33
39
  ```
34
40
 
35
41
  ### AI coding skills
@@ -42,19 +48,24 @@ npx skills add supabase/server
42
48
 
43
49
  ## Quick Start
44
50
 
51
+ 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:
52
+
45
53
  ### Authenticated endpoint
46
54
 
47
55
  ```ts
56
+ // A signed-in user fetches their favorite games.
48
57
  export default {
49
58
  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)
59
+ const { supabase, supabaseAdmin, userClaims, claims, authType } = ctx
60
+ // supabase — RLS-scoped to the authenticated user
61
+ // supabaseAdmin bypasses RLS (service role)
62
+ // userClaims user identity from JWT (id, email, role)
63
+ // claims full JWT claims
64
+ // authType — which auth mode matched
65
+
66
+ // RLS-scoped — this user only sees their own favorites
67
+ const { data: myGames } = await supabase.from('favorite_games').select()
68
+ return Response.json(myGames)
58
69
  }),
59
70
  }
60
71
  ```
@@ -62,6 +73,8 @@ export default {
62
73
  ### Public endpoint (no auth)
63
74
 
64
75
  ```ts
76
+ // The frontend hits this before showing the login screen.
77
+ // allow: 'always' means no credentials required.
65
78
  export default {
66
79
  fetch: withSupabase({ allow: 'always' }, async (_req, _ctx) => {
67
80
  return Response.json({ status: 'ok' })
@@ -72,10 +85,14 @@ export default {
72
85
  ### API key protected
73
86
 
74
87
  ```ts
88
+ // An admin dashboard fetches the list of featured games to curate.
89
+ // Secret key auth (not a user JWT) — supabaseAdmin bypasses RLS.
75
90
  export default {
76
91
  fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
77
- const { data } = await ctx.supabaseAdmin.from('config').select()
78
- return Response.json(data)
92
+ const { data: featuredGames } = await ctx.supabaseAdmin
93
+ .from('featured_games')
94
+ .select()
95
+ return Response.json(featuredGames)
79
96
  }),
80
97
  }
81
98
  ```
@@ -83,14 +100,25 @@ export default {
83
100
  ### Dual auth (user or service)
84
101
 
85
102
  ```ts
103
+ // Users view their own play stats from the app (JWT).
104
+ // A backend service pulls stats for any user (secret key + user_id in body).
86
105
  export default {
87
106
  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')
107
+ const callerIsUser = ctx.authType === 'user'
108
+
109
+ if (callerIsUser) {
110
+ // RLS-scoped — the database enforces "own stats only"
111
+ const { data: myStats } = await ctx.supabase.from('play_stats').select()
112
+ return Response.json(myStats)
113
+ }
114
+
115
+ // Service path — bypass RLS to pull stats for any user
116
+ const { user_id } = await req.json()
117
+ const { data: playStats } = await ctx.supabaseAdmin
118
+ .from('play_stats')
91
119
  .select()
92
- .eq('user_id', userId)
93
- return Response.json(data)
120
+ .eq('user_id', user_id)
121
+ return Response.json(playStats)
94
122
  }),
95
123
  }
96
124
  ```
@@ -98,28 +126,35 @@ export default {
98
126
  ### Server-to-server
99
127
 
100
128
  ```ts
101
- // Only accept the "automations" named secret key
129
+ // A cron job refreshes the "popular this week" list every hour.
130
+ // Named key ("cron") so it can be rotated without touching other services.
102
131
  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 })
132
+ fetch: withSupabase({ allow: 'secret:cron' }, async (_req, ctx) => {
133
+ const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
134
+ const { data: popularThisWeek } = await ctx.supabaseAdmin.rpc(
135
+ 'get_most_favorited_since',
136
+ { since: oneWeekAgo.toISOString(), limit_count: 10 },
137
+ )
138
+ await ctx.supabaseAdmin
139
+ .from('featured_games')
140
+ .upsert(
141
+ popularThisWeek.map((g) => ({ game_id: g.id, reason: 'popular' })),
142
+ )
143
+ return Response.json({ popularThisWeek })
109
144
  }),
110
145
  }
111
146
  ```
112
147
 
113
- The caller sends the secret key in the `apikey` header:
148
+ The cron job sends the named secret key in the `apikey` header:
114
149
 
115
150
  ```ts
116
- await fetch('https://<project>.supabase.co/functions/v1/my-function', {
151
+ const refreshEndpoint =
152
+ 'https://<project>.supabase.co/functions/v1/refresh-popular'
153
+ const cronKey = 'sb_secret_...' // the "cron" named secret key
154
+
155
+ await fetch(refreshEndpoint, {
117
156
  method: 'POST',
118
- headers: {
119
- 'Content-Type': 'application/json',
120
- apikey: 'sb_secret_...', // the "automations" secret key
121
- },
122
- body: JSON.stringify({ taskName: 'cleanup' }),
157
+ headers: { apikey: cronKey },
123
158
  })
124
159
  ```
125
160
 
@@ -132,7 +167,7 @@ await fetch('https://<project>.supabase.co/functions/v1/my-function', {
132
167
  | `"secret"` | Valid secret key | Server-to-server, internal calls |
133
168
  | `"always"` | None | Open endpoints, wrappers that handle their own auth |
134
169
 
135
- Array syntax (`allow: ["user", "secret"]`) accepts multiple auth methods — first match wins.
170
+ 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).
136
171
 
137
172
  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`.
138
173
 
@@ -154,6 +189,7 @@ interface SupabaseContext {
154
189
  userClaims: UserClaims | null // JWT-derived identity (for full User, call supabase.auth.getUser())
155
190
  claims: JWTClaims | null // Present when auth is JWT
156
191
  authType: Allow // Which auth mode matched
192
+ authKeyName?: string | null // Auth key name of the API key that was used for this request
157
193
  }
158
194
  ```
159
195
 
@@ -201,12 +237,14 @@ import { withSupabase } from '@supabase/server/adapters/hono'
201
237
 
202
238
  const app = new Hono()
203
239
 
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)
240
+ // Protected — withSupabase middleware validates the JWT before the handler runs
241
+ app.get('/games', withSupabase({ allow: 'user' }), async (c) => {
242
+ const { supabase } = c.var.supabaseContext
243
+ const { data: myGames } = await supabase.from('favorite_games').select()
244
+ return c.json(myGames)
208
245
  })
209
246
 
247
+ // Public — no middleware means no auth
210
248
  app.get('/health', (c) => c.json({ status: 'ok' }))
211
249
 
212
250
  export default { fetch: app.fetch }
@@ -214,6 +252,56 @@ export default { fetch: app.fetch }
214
252
 
215
253
  The adapter does not handle CORS — use `hono/cors` for that. Per-route auth works naturally by applying the middleware to specific routes.
216
254
 
255
+ ### H3 / Nuxt
256
+
257
+ ```ts
258
+ import { H3 } from 'h3'
259
+ import { withSupabase } from '@supabase/server/adapters/h3'
260
+
261
+ const app = new H3()
262
+
263
+ // Protected — withSupabase validates the JWT before the handler runs
264
+ app.use(withSupabase({ allow: 'user' }))
265
+
266
+ app.get('/games', async (event) => {
267
+ const { supabase } = event.context.supabaseContext
268
+ const { data: myGames } = await supabase.from('favorite_games').select()
269
+ return myGames
270
+ })
271
+
272
+ // Public — no middleware means no auth
273
+ app.get('/health', () => ({ status: 'ok' }))
274
+
275
+ export default { fetch: app.fetch }
276
+ ```
277
+
278
+ For **Nuxt**, use `defineHandler` for file routes:
279
+
280
+ ```ts
281
+ // server/api/games.get.ts
282
+ import { defineHandler } from 'h3'
283
+ import { withSupabase } from '@supabase/server/adapters/h3'
284
+
285
+ export default defineHandler({
286
+ middleware: [withSupabase({ allow: 'user' })],
287
+ handler: async (event) => {
288
+ const { supabase } = event.context.supabaseContext
289
+ return supabase.from('favorite_games').select()
290
+ },
291
+ })
292
+ ```
293
+
294
+ For app-wide auth, register it as a server middleware:
295
+
296
+ ```ts
297
+ // server/middleware/supabase.ts
298
+ import { withSupabase } from '@supabase/server/adapters/h3'
299
+
300
+ export default withSupabase({ allow: 'user' })
301
+ ```
302
+
303
+ The adapter does not handle CORS — use H3's CORS utilities for that.
304
+
217
305
  ## Primitives
218
306
 
219
307
  For when you need more control than `withSupabase` provides — multiple routes with different auth, custom response headers, or building your own wrapper.
@@ -253,9 +341,9 @@ const { data: auth, error } = await verifyCredentials(credentials, {
253
341
  ### createContextClient / createAdminClient
254
342
 
255
343
  ```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
344
+ const userScopedClient = createContextClient(auth.token) // RLS applies as this user
345
+ const anonClient = createContextClient() // RLS applies as anon role
346
+ const adminClient = createAdminClient() // bypasses RLS entirely
259
347
  ```
260
348
 
261
349
  ### createSupabaseContext
@@ -278,6 +366,8 @@ const { data: env, error } = resolveEnv({
278
366
 
279
367
  ### Example: custom multi-route handler
280
368
 
369
+ The same games API and health check from the Hono example, built from primitives instead of a framework:
370
+
281
371
  ```ts
282
372
  import { verifyAuth, createContextClient } from '@supabase/server/core'
283
373
 
@@ -285,11 +375,13 @@ export default {
285
375
  fetch: async (req) => {
286
376
  const url = new URL(req.url)
287
377
 
378
+ // Public — no auth needed
288
379
  if (url.pathname === '/health') {
289
380
  return Response.json({ status: 'ok' })
290
381
  }
291
382
 
292
- if (url.pathname === '/todos') {
383
+ // Protected verify the JWT, then create a user-scoped client
384
+ if (url.pathname === '/games') {
293
385
  const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
294
386
  if (error)
295
387
  return Response.json(
@@ -297,9 +389,11 @@ export default {
297
389
  { status: error.status },
298
390
  )
299
391
 
300
- const supabase = createContextClient(auth.token)
301
- const { data } = await supabase.from('todos').select()
302
- return Response.json(data)
392
+ const userScopedClient = createContextClient(auth.token)
393
+ const { data: myGames } = await userScopedClient
394
+ .from('favorite_games')
395
+ .select()
396
+ return Response.json(myGames)
303
397
  }
304
398
 
305
399
  return new Response('Not found', { status: 404 })
@@ -333,9 +427,10 @@ For other environments, pass overrides via the `env` config option or `resolveEn
333
427
 
334
428
  - **Supabase Edge Functions** — environment variables are auto-injected. Zero config.
335
429
  - **Deno / Bun** — works out of the box with the `export default { fetch }` pattern.
336
- - **Node.js** — use the [Hono adapter](#hono) or [core primitives](#primitives) with your framework of choice.
430
+ - **Node.js** — use the [Hono adapter](#hono), [H3 adapter](#h3--nuxt), or [core primitives](#primitives) with your framework of choice.
337
431
  - **Cloudflare Workers** — enable `nodejs_compat` in `wrangler.toml` or pass env overrides via the `env` config option.
338
- - **Next.js / Nuxt / SvelteKit / Remix** — use core primitives to build a cookie-based auth adapter. See [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md).
432
+ - **Nuxt** — use the [H3 adapter](#h3--nuxt) directly as a server middleware.
433
+ - **Next.js / SvelteKit / Remix** — use core primitives to build a cookie-based auth adapter. See [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md).
339
434
 
340
435
  ## Exports
341
436
 
@@ -344,6 +439,7 @@ For other environments, pass overrides via the `env` config option or `resolveEn
344
439
  | `@supabase/server` | `withSupabase`, `createSupabaseContext` |
345
440
  | `@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `createContextClient`, `createAdminClient`, `resolveEnv` |
346
441
  | `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) |
442
+ | `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) |
347
443
 
348
444
  ## Documentation
349
445
 
@@ -0,0 +1,59 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_create_supabase_context = require('../../create-supabase-context-C_8SbO5w.cjs');
3
+ let h3 = require("h3");
4
+
5
+ //#region src/adapters/h3/middleware.ts
6
+ /**
7
+ * H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
8
+ *
9
+ * Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
10
+ * Throws an `HTTPError` on auth failure.
11
+ *
12
+ * @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
13
+ * @returns An H3 middleware.
14
+ *
15
+ * @example App-wide auth via `app.use()`
16
+ * ```ts
17
+ * import { H3 } from 'h3'
18
+ * import { withSupabase } from '@supabase/server/adapters/h3'
19
+ *
20
+ * const app = new H3()
21
+ * app.use(withSupabase({ allow: 'user' }))
22
+ *
23
+ * app.get('/games', async (event) => {
24
+ * const { supabase } = event.context.supabaseContext
25
+ * return supabase.from('favorite_games').select()
26
+ * })
27
+ *
28
+ * export default { fetch: app.fetch }
29
+ * ```
30
+ *
31
+ * @example Per-route auth via `defineHandler`
32
+ * ```ts
33
+ * import { defineHandler } from 'h3'
34
+ * import { withSupabase } from '@supabase/server/adapters/h3'
35
+ *
36
+ * export default defineHandler({
37
+ * middleware: [withSupabase({ allow: 'user' })],
38
+ * handler: async (event) => {
39
+ * const { supabase } = event.context.supabaseContext
40
+ * return supabase.from('favorite_games').select()
41
+ * },
42
+ * })
43
+ * ```
44
+ */
45
+ function withSupabase(config) {
46
+ return (0, h3.defineMiddleware)(async (event, next) => {
47
+ if (event.context.supabaseContext) return next();
48
+ const { data: ctx, error } = await require_create_supabase_context.createSupabaseContext(event.req, config);
49
+ if (error) throw new h3.HTTPError(error.message, {
50
+ status: error.status,
51
+ cause: error
52
+ });
53
+ event.context.supabaseContext = ctx;
54
+ return next();
55
+ });
56
+ }
57
+
58
+ //#endregion
59
+ exports.withSupabase = withSupabase;
@@ -0,0 +1,51 @@
1
+ import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DqhOaSlC.cjs";
2
+ import { Middleware } from "h3";
3
+
4
+ //#region src/adapters/h3/middleware.d.ts
5
+ /**
6
+ * H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
7
+ *
8
+ * Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
9
+ * Throws an `HTTPError` on auth failure.
10
+ *
11
+ * @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
12
+ * @returns An H3 middleware.
13
+ *
14
+ * @example App-wide auth via `app.use()`
15
+ * ```ts
16
+ * import { H3 } from 'h3'
17
+ * import { withSupabase } from '@supabase/server/adapters/h3'
18
+ *
19
+ * const app = new H3()
20
+ * app.use(withSupabase({ allow: 'user' }))
21
+ *
22
+ * app.get('/games', async (event) => {
23
+ * const { supabase } = event.context.supabaseContext
24
+ * return supabase.from('favorite_games').select()
25
+ * })
26
+ *
27
+ * export default { fetch: app.fetch }
28
+ * ```
29
+ *
30
+ * @example Per-route auth via `defineHandler`
31
+ * ```ts
32
+ * import { defineHandler } from 'h3'
33
+ * import { withSupabase } from '@supabase/server/adapters/h3'
34
+ *
35
+ * export default defineHandler({
36
+ * middleware: [withSupabase({ allow: 'user' })],
37
+ * handler: async (event) => {
38
+ * const { supabase } = event.context.supabaseContext
39
+ * return supabase.from('favorite_games').select()
40
+ * },
41
+ * })
42
+ * ```
43
+ */
44
+ declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Middleware;
45
+ declare module 'h3' {
46
+ interface H3EventContext {
47
+ supabaseContext: SupabaseContext;
48
+ }
49
+ }
50
+ //#endregion
51
+ export { withSupabase };
@@ -0,0 +1,51 @@
1
+ import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DKe8uOwI.mjs";
2
+ import { Middleware } from "h3";
3
+
4
+ //#region src/adapters/h3/middleware.d.ts
5
+ /**
6
+ * H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
7
+ *
8
+ * Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
9
+ * Throws an `HTTPError` on auth failure.
10
+ *
11
+ * @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
12
+ * @returns An H3 middleware.
13
+ *
14
+ * @example App-wide auth via `app.use()`
15
+ * ```ts
16
+ * import { H3 } from 'h3'
17
+ * import { withSupabase } from '@supabase/server/adapters/h3'
18
+ *
19
+ * const app = new H3()
20
+ * app.use(withSupabase({ allow: 'user' }))
21
+ *
22
+ * app.get('/games', async (event) => {
23
+ * const { supabase } = event.context.supabaseContext
24
+ * return supabase.from('favorite_games').select()
25
+ * })
26
+ *
27
+ * export default { fetch: app.fetch }
28
+ * ```
29
+ *
30
+ * @example Per-route auth via `defineHandler`
31
+ * ```ts
32
+ * import { defineHandler } from 'h3'
33
+ * import { withSupabase } from '@supabase/server/adapters/h3'
34
+ *
35
+ * export default defineHandler({
36
+ * middleware: [withSupabase({ allow: 'user' })],
37
+ * handler: async (event) => {
38
+ * const { supabase } = event.context.supabaseContext
39
+ * return supabase.from('favorite_games').select()
40
+ * },
41
+ * })
42
+ * ```
43
+ */
44
+ declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Middleware;
45
+ declare module 'h3' {
46
+ interface H3EventContext {
47
+ supabaseContext: SupabaseContext;
48
+ }
49
+ }
50
+ //#endregion
51
+ export { withSupabase };
@@ -0,0 +1,58 @@
1
+ import { t as createSupabaseContext } from "../../create-supabase-context-DXD5rxi1.mjs";
2
+ import { HTTPError, defineMiddleware } from "h3";
3
+
4
+ //#region src/adapters/h3/middleware.ts
5
+ /**
6
+ * H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
7
+ *
8
+ * Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
9
+ * Throws an `HTTPError` on auth failure.
10
+ *
11
+ * @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
12
+ * @returns An H3 middleware.
13
+ *
14
+ * @example App-wide auth via `app.use()`
15
+ * ```ts
16
+ * import { H3 } from 'h3'
17
+ * import { withSupabase } from '@supabase/server/adapters/h3'
18
+ *
19
+ * const app = new H3()
20
+ * app.use(withSupabase({ allow: 'user' }))
21
+ *
22
+ * app.get('/games', async (event) => {
23
+ * const { supabase } = event.context.supabaseContext
24
+ * return supabase.from('favorite_games').select()
25
+ * })
26
+ *
27
+ * export default { fetch: app.fetch }
28
+ * ```
29
+ *
30
+ * @example Per-route auth via `defineHandler`
31
+ * ```ts
32
+ * import { defineHandler } from 'h3'
33
+ * import { withSupabase } from '@supabase/server/adapters/h3'
34
+ *
35
+ * export default defineHandler({
36
+ * middleware: [withSupabase({ allow: 'user' })],
37
+ * handler: async (event) => {
38
+ * const { supabase } = event.context.supabaseContext
39
+ * return supabase.from('favorite_games').select()
40
+ * },
41
+ * })
42
+ * ```
43
+ */
44
+ function withSupabase(config) {
45
+ return defineMiddleware(async (event, next) => {
46
+ if (event.context.supabaseContext) return next();
47
+ const { data: ctx, error } = await createSupabaseContext(event.req, config);
48
+ if (error) throw new HTTPError(error.message, {
49
+ status: error.status,
50
+ cause: error
51
+ });
52
+ event.context.supabaseContext = ctx;
53
+ return next();
54
+ });
55
+ }
56
+
57
+ //#endregion
58
+ export { withSupabase };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_create_supabase_context = require('../../create-supabase-context--VqMJpDu.cjs');
2
+ const require_create_supabase_context = require('../../create-supabase-context-C_8SbO5w.cjs');
3
3
  let hono_http_exception = require("hono/http-exception");
4
4
  let hono_factory = require("hono/factory");
5
5
 
@@ -1,5 +1,5 @@
1
- import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DxTr0Qum.cjs";
2
- import * as hono_types0 from "hono/types";
1
+ import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DqhOaSlC.cjs";
2
+ import { MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/adapters/hono/middleware.d.ts
5
5
  /**
@@ -28,10 +28,10 @@ import * as hono_types0 from "hono/types";
28
28
  * export default { fetch: app.fetch }
29
29
  * ```
30
30
  */
31
- declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): hono_types0.MiddlewareHandler<{
31
+ declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): MiddlewareHandler<{
32
32
  Variables: {
33
33
  supabaseContext: SupabaseContext;
34
34
  };
35
- }, string, {}, Response>;
35
+ }>;
36
36
  //#endregion
37
37
  export { withSupabase };
@@ -1,5 +1,5 @@
1
- import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-CbC-wBUe.mjs";
2
- import * as hono_types0 from "hono/types";
1
+ import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DKe8uOwI.mjs";
2
+ import { MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/adapters/hono/middleware.d.ts
5
5
  /**
@@ -28,10 +28,10 @@ import * as hono_types0 from "hono/types";
28
28
  * export default { fetch: app.fetch }
29
29
  * ```
30
30
  */
31
- declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): hono_types0.MiddlewareHandler<{
31
+ declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): MiddlewareHandler<{
32
32
  Variables: {
33
33
  supabaseContext: SupabaseContext;
34
34
  };
35
- }, string, {}, Response>;
35
+ }>;
36
36
  //#endregion
37
37
  export { withSupabase };
@@ -1,4 +1,4 @@
1
- import { t as createSupabaseContext } from "../../create-supabase-context-B3Uzt_3I.mjs";
1
+ import { t as createSupabaseContext } from "../../create-supabase-context-DXD5rxi1.mjs";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import { createMiddleware } from "hono/factory";
4
4
 
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_verify_auth = require('../verify-auth-DrgvEuKo.cjs');
2
+ const require_verify_auth = require('../verify-auth-C4zqDlfj.cjs');
3
3
 
4
4
  exports.createAdminClient = require_verify_auth.createAdminClient;
5
5
  exports.createContextClient = require_verify_auth.createContextClient;
@@ -1,5 +1,5 @@
1
- import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-DxTr0Qum.cjs";
2
- import { i as EnvError, t as AuthError } from "../errors-O2ugIMec.cjs";
1
+ import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-DqhOaSlC.cjs";
2
+ import { i as EnvError, t as AuthError } from "../errors-Dyj5Cjt6.cjs";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
4
 
5
5
  //#region src/core/resolve-env.d.ts
@@ -72,8 +72,11 @@ interface VerifyCredentialsOptions {
72
72
  /**
73
73
  * Verifies pre-extracted credentials against one or more allowed auth modes.
74
74
  *
75
- * Tries each mode in order — first match wins. Use {@link verifyAuth} to extract
76
- * and verify in a single call.
75
+ * Tries each mode in order — first match wins. A mode is only tried when its
76
+ * credential is present; a JWT that is present but fails verification
77
+ * short-circuits the chain with `InvalidCredentialsError` instead of falling
78
+ * through to the next mode. Use {@link verifyAuth} to extract and verify in a
79
+ * single call.
77
80
  *
78
81
  * @param credentials - The credentials to verify (from {@link extractCredentials}).
79
82
  * @param options - Allowed auth modes and optional env overrides.
@@ -1,5 +1,5 @@
1
- import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-CbC-wBUe.mjs";
2
- import { i as EnvError, t as AuthError } from "../errors-CAH-RRA3.mjs";
1
+ import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-DKe8uOwI.mjs";
2
+ import { i as EnvError, t as AuthError } from "../errors-m42mkqhD.mjs";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
4
 
5
5
  //#region src/core/resolve-env.d.ts
@@ -72,8 +72,11 @@ interface VerifyCredentialsOptions {
72
72
  /**
73
73
  * Verifies pre-extracted credentials against one or more allowed auth modes.
74
74
  *
75
- * Tries each mode in order — first match wins. Use {@link verifyAuth} to extract
76
- * and verify in a single call.
75
+ * Tries each mode in order — first match wins. A mode is only tried when its
76
+ * credential is present; a JWT that is present but fails verification
77
+ * short-circuits the chain with `InvalidCredentialsError` instead of falling
78
+ * through to the next mode. Use {@link verifyAuth} to extract and verify in a
79
+ * single call.
77
80
  *
78
81
  * @param credentials - The credentials to verify (from {@link extractCredentials}).
79
82
  * @param options - Allowed auth modes and optional env overrides.
@@ -1,3 +1,3 @@
1
- import { a as createAdminClient, i as createContextClient, n as verifyCredentials, o as resolveEnv, r as extractCredentials, t as verifyAuth } from "../verify-auth-Bt2uGltH.mjs";
1
+ import { a as createAdminClient, i as createContextClient, n as verifyCredentials, o as resolveEnv, r as extractCredentials, t as verifyAuth } from "../verify-auth-CxFZy9rl.mjs";
2
2
 
3
3
  export { createAdminClient, createContextClient, extractCredentials, resolveEnv, verifyAuth, verifyCredentials };
@@ -1,4 +1,4 @@
1
- const require_verify_auth = require('./verify-auth-DrgvEuKo.cjs');
1
+ const require_verify_auth = require('./verify-auth-C4zqDlfj.cjs');
2
2
 
3
3
  //#region src/create-supabase-context.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as createAdminClient, f as Errors, i as createContextClient, l as CreateSupabaseClientError, s as AuthError, t as verifyAuth, u as EnvError } from "./verify-auth-Bt2uGltH.mjs";
1
+ import { a as createAdminClient, f as Errors, i as createContextClient, l as CreateSupabaseClientError, s as AuthError, t as verifyAuth, u as EnvError } from "./verify-auth-CxFZy9rl.mjs";
2
2
 
3
3
  //#region src/create-supabase-context.ts
4
4
  /**
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_verify_auth = require('./verify-auth-DrgvEuKo.cjs');
3
- const require_create_supabase_context = require('./create-supabase-context--VqMJpDu.cjs');
2
+ const require_verify_auth = require('./verify-auth-C4zqDlfj.cjs');
3
+ const require_create_supabase_context = require('./create-supabase-context-C_8SbO5w.cjs');
4
4
  let _supabase_supabase_js_cors = require("@supabase/supabase-js/cors");
5
5
 
6
6
  //#region src/cors.ts
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-DxTr0Qum.cjs";
2
- import { a as EnvGenericError, c as MissingDefaultPublishableKeyError, d as MissingSecretKeyError, f as MissingSupabaseURLError, i as EnvError, l as MissingDefaultSecretKeyError, n as AuthGenericError, o as Errors, r as CreateSupabaseClientError, s as InvalidCredentialsError, t as AuthError, u as MissingPublishableKeyError } from "./errors-O2ugIMec.cjs";
1
+ import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-DqhOaSlC.cjs";
2
+ import { a as EnvGenericError, c as MissingDefaultPublishableKeyError, d as MissingSecretKeyError, f as MissingSupabaseURLError, i as EnvError, l as MissingDefaultSecretKeyError, n as AuthGenericError, o as Errors, r as CreateSupabaseClientError, s as InvalidCredentialsError, t as AuthError, u as MissingPublishableKeyError } from "./errors-Dyj5Cjt6.cjs";
3
3
 
4
4
  //#region src/with-supabase.d.ts
5
5
  /**
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-CbC-wBUe.mjs";
2
- import { a as EnvGenericError, c as MissingDefaultPublishableKeyError, d as MissingSecretKeyError, f as MissingSupabaseURLError, i as EnvError, l as MissingDefaultSecretKeyError, n as AuthGenericError, o as Errors, r as CreateSupabaseClientError, s as InvalidCredentialsError, t as AuthError, u as MissingPublishableKeyError } from "./errors-CAH-RRA3.mjs";
1
+ import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-DKe8uOwI.mjs";
2
+ import { a as EnvGenericError, c as MissingDefaultPublishableKeyError, d as MissingSecretKeyError, f as MissingSupabaseURLError, i as EnvError, l as MissingDefaultSecretKeyError, n as AuthGenericError, o as Errors, r as CreateSupabaseClientError, s as InvalidCredentialsError, t as AuthError, u as MissingPublishableKeyError } from "./errors-m42mkqhD.mjs";
3
3
 
4
4
  //#region src/with-supabase.d.ts
5
5
  /**
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as MissingSecretKeyError, c as AuthGenericError, d as EnvGenericError, f as Errors, g as MissingPublishableKeyError, h as MissingDefaultSecretKeyError, l as CreateSupabaseClientError, m as MissingDefaultPublishableKeyError, p as InvalidCredentialsError, s as AuthError, u as EnvError, v as MissingSupabaseURLError } from "./verify-auth-Bt2uGltH.mjs";
2
- import { t as createSupabaseContext } from "./create-supabase-context-B3Uzt_3I.mjs";
1
+ import { _ as MissingSecretKeyError, c as AuthGenericError, d as EnvGenericError, f as Errors, g as MissingPublishableKeyError, h as MissingDefaultSecretKeyError, l as CreateSupabaseClientError, m as MissingDefaultPublishableKeyError, p as InvalidCredentialsError, s as AuthError, u as EnvError, v as MissingSupabaseURLError } from "./verify-auth-CxFZy9rl.mjs";
2
+ import { t as createSupabaseContext } from "./create-supabase-context-DXD5rxi1.mjs";
3
3
  import { corsHeaders } from "@supabase/supabase-js/cors";
4
4
 
5
5
  //#region src/cors.ts
@@ -14,7 +14,10 @@ import { SupabaseClient, SupabaseClientOptions } from "@supabase/supabase-js";
14
14
  * // Single mode
15
15
  * withSupabase({ allow: 'user' }, handler)
16
16
  *
17
- * // Multiple modes — the first match wins
17
+ * // Multiple modes — the first match wins.
18
+ * // A mode is tried only when its credential is present; a JWT that is
19
+ * // present but fails verification rejects immediately rather than falling
20
+ * // through to the next mode.
18
21
  * withSupabase({ allow: ['user', 'public'] }, handler)
19
22
  * ```
20
23
  */
@@ -182,6 +185,8 @@ interface UserClaims {
182
185
  interface WithSupabaseConfig {
183
186
  /**
184
187
  * Auth mode(s) to accept. Modes are tried in order — the first match wins.
188
+ * A mode falls through only when its credential is absent; a present-but-invalid
189
+ * JWT short-circuits the chain with `InvalidCredentialsError`.
185
190
  *
186
191
  * @defaultValue `"user"`
187
192
  */
@@ -14,7 +14,10 @@ import { SupabaseClient, SupabaseClientOptions } from "@supabase/supabase-js";
14
14
  * // Single mode
15
15
  * withSupabase({ allow: 'user' }, handler)
16
16
  *
17
- * // Multiple modes — the first match wins
17
+ * // Multiple modes — the first match wins.
18
+ * // A mode is tried only when its credential is present; a JWT that is
19
+ * // present but fails verification rejects immediately rather than falling
20
+ * // through to the next mode.
18
21
  * withSupabase({ allow: ['user', 'public'] }, handler)
19
22
  * ```
20
23
  */
@@ -182,6 +185,8 @@ interface UserClaims {
182
185
  interface WithSupabaseConfig {
183
186
  /**
184
187
  * Auth mode(s) to accept. Modes are tried in order — the first match wins.
188
+ * A mode falls through only when its credential is absent; a present-but-invalid
189
+ * JWT short-circuits the chain with `InvalidCredentialsError`.
185
190
  *
186
191
  * @defaultValue `"user"`
187
192
  */
@@ -388,9 +388,15 @@ function claimsToUserClaims(claims) {
388
388
  userMetadata: claims.user_metadata
389
389
  };
390
390
  }
391
+ const INVALID = Symbol("invalid");
391
392
  /**
392
393
  * Attempts to authenticate credentials against a single auth mode.
393
- * Returns the {@link AuthResult} on success, or `null` if the mode doesn't match.
394
+ *
395
+ * Returns:
396
+ * - `AuthResult` on success.
397
+ * - `null` if the mode doesn't apply (no relevant credential present — safe to try the next mode).
398
+ * - `INVALID` if a credential was present but failed verification (must reject immediately).
399
+ *
394
400
  * @internal
395
401
  */
396
402
  async function tryMode(mode, credentials, env) {
@@ -457,7 +463,7 @@ async function tryMode(mode, credentials, env) {
457
463
  try {
458
464
  const jwkSet = (0, jose.createLocalJWKSet)(env.jwks);
459
465
  const { payload } = await (0, jose.jwtVerify)(credentials.token, jwkSet);
460
- if (typeof payload.sub !== "string") return null;
466
+ if (typeof payload.sub !== "string") return INVALID;
461
467
  const claims = payload;
462
468
  return {
463
469
  authType: "user",
@@ -467,7 +473,7 @@ async function tryMode(mode, credentials, env) {
467
473
  keyName: null
468
474
  };
469
475
  } catch {
470
- return null;
476
+ return INVALID;
471
477
  }
472
478
  default: return null;
473
479
  }
@@ -475,8 +481,11 @@ async function tryMode(mode, credentials, env) {
475
481
  /**
476
482
  * Verifies pre-extracted credentials against one or more allowed auth modes.
477
483
  *
478
- * Tries each mode in order — first match wins. Use {@link verifyAuth} to extract
479
- * and verify in a single call.
484
+ * Tries each mode in order — first match wins. A mode is only tried when its
485
+ * credential is present; a JWT that is present but fails verification
486
+ * short-circuits the chain with `InvalidCredentialsError` instead of falling
487
+ * through to the next mode. Use {@link verifyAuth} to extract and verify in a
488
+ * single call.
480
489
  *
481
490
  * @param credentials - The credentials to verify (from {@link extractCredentials}).
482
491
  * @param options - Allowed auth modes and optional env overrides.
@@ -502,6 +511,10 @@ async function verifyCredentials(credentials, options) {
502
511
  const modes = Array.isArray(options.allow) ? options.allow : [options.allow];
503
512
  for (const mode of modes) {
504
513
  const result = await tryMode(mode, credentials, env);
514
+ if (result === INVALID) return {
515
+ data: null,
516
+ error: Errors[InvalidCredentialsError]()
517
+ };
505
518
  if (result) return {
506
519
  data: result,
507
520
  error: null
@@ -388,9 +388,15 @@ function claimsToUserClaims(claims) {
388
388
  userMetadata: claims.user_metadata
389
389
  };
390
390
  }
391
+ const INVALID = Symbol("invalid");
391
392
  /**
392
393
  * Attempts to authenticate credentials against a single auth mode.
393
- * Returns the {@link AuthResult} on success, or `null` if the mode doesn't match.
394
+ *
395
+ * Returns:
396
+ * - `AuthResult` on success.
397
+ * - `null` if the mode doesn't apply (no relevant credential present — safe to try the next mode).
398
+ * - `INVALID` if a credential was present but failed verification (must reject immediately).
399
+ *
394
400
  * @internal
395
401
  */
396
402
  async function tryMode(mode, credentials, env) {
@@ -457,7 +463,7 @@ async function tryMode(mode, credentials, env) {
457
463
  try {
458
464
  const jwkSet = createLocalJWKSet(env.jwks);
459
465
  const { payload } = await jwtVerify(credentials.token, jwkSet);
460
- if (typeof payload.sub !== "string") return null;
466
+ if (typeof payload.sub !== "string") return INVALID;
461
467
  const claims = payload;
462
468
  return {
463
469
  authType: "user",
@@ -467,7 +473,7 @@ async function tryMode(mode, credentials, env) {
467
473
  keyName: null
468
474
  };
469
475
  } catch {
470
- return null;
476
+ return INVALID;
471
477
  }
472
478
  default: return null;
473
479
  }
@@ -475,8 +481,11 @@ async function tryMode(mode, credentials, env) {
475
481
  /**
476
482
  * Verifies pre-extracted credentials against one or more allowed auth modes.
477
483
  *
478
- * Tries each mode in order — first match wins. Use {@link verifyAuth} to extract
479
- * and verify in a single call.
484
+ * Tries each mode in order — first match wins. A mode is only tried when its
485
+ * credential is present; a JWT that is present but fails verification
486
+ * short-circuits the chain with `InvalidCredentialsError` instead of falling
487
+ * through to the next mode. Use {@link verifyAuth} to extract and verify in a
488
+ * single call.
480
489
  *
481
490
  * @param credentials - The credentials to verify (from {@link extractCredentials}).
482
491
  * @param options - Allowed auth modes and optional env overrides.
@@ -502,6 +511,10 @@ async function verifyCredentials(credentials, options) {
502
511
  const modes = Array.isArray(options.allow) ? options.allow : [options.allow];
503
512
  for (const mode of modes) {
504
513
  const result = await tryMode(mode, credentials, env);
514
+ if (result === INVALID) return {
515
+ data: null,
516
+ error: Errors[InvalidCredentialsError]()
517
+ };
505
518
  if (result) return {
506
519
  data: result,
507
520
  error: null
@@ -291,17 +291,17 @@ class AuthError extends Error {
291
291
 
292
292
  ## Error Code Constants
293
293
 
294
- | Constant | Value | Class | Meaning |
295
- | ----------------------------------- | ----------------------------------- | ----------- | --------------------------------- |
296
- | `EnvGenericError` | `'ENV_ERROR'` | `EnvError` | Generic environment error |
297
- | `MissingSupabaseURLError` | `'MISSING_SUPABASE_URL'` | `EnvError` | `SUPABASE_URL` not set |
298
- | `MissingPublishableKeyError` | `'MISSING_PUBLISHABLE_KEY'` | `EnvError` | Named publishable key not found |
299
- | `MissingDefaultPublishableKeyError` | `'MISSING_DEFAULT_PUBLISHABLE_KEY'` | `EnvError` | No default publishable key |
300
- | `MissingSecretKeyError` | `'MISSING_SECRET_KEY'` | `EnvError` | Named secret key not found |
301
- | `MissingDefaultSecretKeyError` | `'MISSING_DEFAULT_SECRET_KEY'` | `EnvError` | No default secret key |
302
- | `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error |
303
- | `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | No credential matched |
304
- | `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth |
294
+ | Constant | Value | Class | Meaning |
295
+ | ----------------------------------- | ----------------------------------- | ----------- | ------------------------------------------------- |
296
+ | `EnvGenericError` | `'ENV_ERROR'` | `EnvError` | Generic environment error |
297
+ | `MissingSupabaseURLError` | `'MISSING_SUPABASE_URL'` | `EnvError` | `SUPABASE_URL` not set |
298
+ | `MissingPublishableKeyError` | `'MISSING_PUBLISHABLE_KEY'` | `EnvError` | Named publishable key not found |
299
+ | `MissingDefaultPublishableKeyError` | `'MISSING_DEFAULT_PUBLISHABLE_KEY'` | `EnvError` | No default publishable key |
300
+ | `MissingSecretKeyError` | `'MISSING_SECRET_KEY'` | `EnvError` | Named secret key not found |
301
+ | `MissingDefaultSecretKeyError` | `'MISSING_DEFAULT_SECRET_KEY'` | `EnvError` | No default secret key |
302
+ | `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error |
303
+ | `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | No credential matched, or JWT failed verification |
304
+ | `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth |
305
305
 
306
306
  ---
307
307
 
@@ -147,6 +147,8 @@ export default {
147
147
 
148
148
  A request with a valid JWT matches `'user'`. A request with a valid secret key matches `'secret'`. A request with neither is rejected.
149
149
 
150
+ **Fallthrough vs rejection.** A mode is only "tried" when its credential is actually present. A request with no `Authorization` header moves on to the next mode. But if a JWT _is_ present and fails verification (malformed, expired, wrong signature, or missing a `sub` claim), the request is rejected immediately with `InvalidCredentialsError` — it will not silently fall through to `'public'`, `'secret'`, or `'always'`. The same rule applies on the API-key side: `'public'` and `'secret'` fall through only when no `apikey` header is sent. This prevents a bad credential from being downgraded to a less-privileged auth mode.
151
+
150
152
  ## Named key syntax
151
153
 
152
154
  When your project has multiple API keys (e.g., separate keys for web, mobile, and internal services), use the colon syntax to validate against a specific named key.
@@ -198,6 +200,6 @@ withSupabase({ allow: ['user', 'public:web'] }, async (_req, ctx) => {
198
200
 
199
201
  1. `extractCredentials(request)` reads `Authorization: Bearer <token>` and `apikey` from headers
200
202
  2. Each mode in `allow` is tried in order against the extracted credentials
201
- 3. First match wins — returns an `AuthResult` with `authType`, `token`, `userClaims`, `claims`, and `keyName`
203
+ 3. First match wins — returns an `AuthResult` with `authType`, `token`, `userClaims`, `claims`, and `keyName`. A mode falls through to the next only when its credential is absent; a credential that is present but invalid terminates the chain with `InvalidCredentialsError`.
202
204
  4. The auth result is used to create scoped clients (`supabase` with the user's token, `supabaseAdmin` with the secret key)
203
205
  5. Everything is bundled into a `SupabaseContext` and passed to your handler
@@ -21,11 +21,11 @@ Thrown when a required environment variable is missing or malformed. Always `sta
21
21
 
22
22
  Thrown when authentication or authorization fails. Status is `401` for invalid credentials, `500` for server-side auth failures.
23
23
 
24
- | Code | Status | Meaning |
25
- | ------------------------------ | ------ | ------------------------------------------- |
26
- | `INVALID_CREDENTIALS` | 401 | No credential matched any allowed auth mode |
27
- | `CREATE_SUPABASE_CLIENT_ERROR` | 500 | Auth succeeded but client creation failed |
28
- | `AUTH_ERROR` | 401 | Generic authentication error |
24
+ | Code | Status | Meaning |
25
+ | ------------------------------ | ------ | ----------------------------------------------------------------------------------------- |
26
+ | `INVALID_CREDENTIALS` | 401 | No credential matched any allowed auth mode, or a JWT was present but failed verification |
27
+ | `CREATE_SUPABASE_CLIENT_ERROR` | 500 | Auth succeeded but client creation failed |
28
+ | `AUTH_ERROR` | 401 | Generic authentication error |
29
29
 
30
30
  ## How errors surface in each layer
31
31
 
package/docs/security.md CHANGED
@@ -56,10 +56,12 @@ JWT verification in `user` mode works as follows:
56
56
  2. The token is verified against the JWKS from the `SUPABASE_JWKS` environment variable
57
57
  3. Verification uses `jose`'s `jwtVerify` with a **local** key set — there are no network calls to a JWKS endpoint
58
58
  4. The token must contain a `sub` (subject) claim to be considered valid
59
- 5. On success, the decoded claims are available as `ctx.user` and `ctx.claims`
59
+ 5. On success, the decoded claims are available as `ctx.userClaims` and `ctx.claims`
60
60
 
61
61
  If JWKS is not configured (`SUPABASE_JWKS` is missing or malformed), `user` mode is unavailable and will always reject requests.
62
62
 
63
+ **No silent downgrade.** When `user` is combined with other modes (e.g. `allow: ['user', 'public']`), a JWT that is present but fails verification rejects the request with `InvalidCredentialsError` — it does not fall through to the next mode. This prevents a bad token paired with a valid `apikey` (or with `'always'`) from being silently downgraded to a less-privileged auth mode. Requests that simply omit the `Authorization` header still fall through as expected.
64
+
63
65
  ## CORS handling
64
66
 
65
67
  `withSupabase` handles CORS automatically:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/server",
3
- "version": "0.1.4",
3
+ "version": "0.2.0-rc.45",
4
4
  "description": "Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.",
5
5
  "keywords": [
6
6
  "edge",
@@ -34,6 +34,11 @@
34
34
  "import": "./dist/adapters/hono/index.mjs",
35
35
  "require": "./dist/adapters/hono/index.cjs"
36
36
  },
37
+ "./adapters/h3": {
38
+ "types": "./dist/adapters/h3/index.d.mts",
39
+ "import": "./dist/adapters/h3/index.mjs",
40
+ "require": "./dist/adapters/h3/index.cjs"
41
+ },
37
42
  "./package.json": "./package.json"
38
43
  },
39
44
  "main": "./dist/index.cjs",
@@ -69,9 +74,13 @@
69
74
  },
70
75
  "peerDependencies": {
71
76
  "@supabase/supabase-js": "^2.0.0",
72
- "hono": "^4.0.0"
77
+ "hono": "^4.0.0",
78
+ "h3": "^2.0.0"
73
79
  },
74
80
  "peerDependenciesMeta": {
81
+ "h3": {
82
+ "optional": true
83
+ },
75
84
  "hono": {
76
85
  "optional": true
77
86
  }
@@ -81,6 +90,7 @@
81
90
  "@commitlint/config-conventional": "^20.4.2",
82
91
  "@supabase/supabase-js": "^2.98.0",
83
92
  "eslint": "^10.0.2",
93
+ "h3": "2.0.1-rc.20",
84
94
  "hono": "^4.12.5",
85
95
  "prettier": "3.8.1",
86
96
  "pretty-quick": "^4.2.2",
@@ -5,6 +5,8 @@ description: Use when writing server-side code with Supabase — Edge Functions,
5
5
 
6
6
  # @supabase/server
7
7
 
8
+ > **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).
9
+
8
10
  > **This is a brand new package.** There is no information available online yet — no blog posts, no Stack Overflow answers, no tutorials. Do not search the web for usage examples. Rely exclusively on the documentation files listed below and the source code in this repository.
9
11
 
10
12
  > **Do not use legacy Supabase keys.** The `anon` key and `service_role` key (env vars `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`) are legacy and will be deprecated. Do not use them unless the user explicitly asks. Always use the new API keys:
@@ -22,6 +24,7 @@ Server-side utilities for Supabase. Handles auth, client creation, and context i
22
24
 
23
25
  - Wraps fetch handlers with credential verification, CORS, and pre-configured Supabase clients
24
26
  - Supports 4 auth modes: `user` (JWT), `public` (publishable key), `secret` (secret key), `always` (none)
27
+ - Array syntax (`allow: ['user', 'secret']`) is first-match-wins. A present-but-invalid JWT rejects with `InvalidCredentialsError` — it does not silently downgrade to the next mode.
25
28
  - Provides composable core primitives for custom auth flows and framework integration
26
29
  - Includes a Hono adapter for per-route auth
27
30
 
@@ -197,6 +200,8 @@ Use `allow: 'secret'` to accept any secret key, or `allow: 'secret:name'` to req
197
200
 
198
201
  **Never use `allow: 'always'` for endpoints that read or write user data without verifying who the caller is.**
199
202
 
203
+ **On `allow: ['user', 'always']`.** A stale or malformed JWT on such an endpoint is rejected with `InvalidCredentialsError` — it is not silently downgraded to anonymous. Callers that might hold a cached/expired token should either omit the `Authorization` header entirely or refresh before calling. If the goal is "anonymous unless a valid user is signed in," this is the correct behavior; if the goal is truly "accept anything," use `allow: 'always'` on its own.
204
+
200
205
  ## Edge Function recipes
201
206
 
202
207
  ### Function-to-function calls