@supabase/server 0.2.0 → 1.0.0

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 (38) hide show
  1. package/README.md +93 -92
  2. package/dist/adapters/h3/index.cjs +3 -3
  3. package/dist/adapters/h3/index.d.cts +3 -3
  4. package/dist/adapters/h3/index.d.mts +3 -3
  5. package/dist/adapters/h3/index.mjs +3 -3
  6. package/dist/adapters/hono/index.cjs +2 -2
  7. package/dist/adapters/hono/index.d.cts +2 -2
  8. package/dist/adapters/hono/index.d.mts +2 -2
  9. package/dist/adapters/hono/index.mjs +2 -2
  10. package/dist/core/index.cjs +1 -1
  11. package/dist/core/index.d.cts +25 -9
  12. package/dist/core/index.d.mts +25 -9
  13. package/dist/core/index.mjs +1 -1
  14. package/dist/{create-supabase-context-C_8SbO5w.cjs → create-supabase-context-B-2NDJhL.cjs} +10 -9
  15. package/dist/{create-supabase-context-DXD5rxi1.mjs → create-supabase-context-BBZtr3D2.mjs} +10 -9
  16. package/dist/{errors-Dyj5Cjt6.d.cts → errors-0dbzn5gA.d.mts} +1 -1
  17. package/dist/{errors-m42mkqhD.d.mts → errors-CZFEYnV_.d.cts} +1 -1
  18. package/dist/index.cjs +3 -3
  19. package/dist/index.d.cts +5 -5
  20. package/dist/index.d.mts +5 -5
  21. package/dist/index.mjs +3 -3
  22. package/dist/{types-DKe8uOwI.d.mts → types-B2yXZjmG.d.mts} +40 -23
  23. package/dist/{types-DqhOaSlC.d.cts → types-u7fYLtzC.d.cts} +40 -23
  24. package/dist/{verify-auth-C4zqDlfj.cjs → verify-auth-BKZK83Y8.cjs} +66 -34
  25. package/dist/{verify-auth-CxFZy9rl.mjs → verify-auth-CZQd36s0.mjs} +66 -34
  26. package/docs/adapters/h3.md +180 -0
  27. package/docs/{hono-adapter.md → adapters/hono.md} +14 -25
  28. package/docs/api-reference.md +28 -15
  29. package/docs/auth-modes.md +38 -34
  30. package/docs/core-primitives.md +13 -13
  31. package/docs/environment-variables.md +17 -17
  32. package/docs/error-handling.md +4 -4
  33. package/docs/getting-started.md +17 -17
  34. package/docs/security.md +15 -15
  35. package/docs/ssr-frameworks.md +148 -172
  36. package/docs/typescript-generics.md +6 -6
  37. package/package.json +5 -3
  38. package/skills/supabase-server/SKILL.md +51 -44
@@ -58,7 +58,7 @@ const EnvErrorMap = {
58
58
  * ```ts
59
59
  * import { AuthError, createSupabaseContext } from '@supabase/server'
60
60
  *
61
- * const { data: ctx, error } = await createSupabaseContext(request, { allow: 'user' })
61
+ * const { data: ctx, error } = await createSupabaseContext(request, { auth: 'user' })
62
62
  * if (error) {
63
63
  * // error is an AuthError
64
64
  * return Response.json(
@@ -249,7 +249,7 @@ function createAdminClient(options) {
249
249
  *
250
250
  * @example
251
251
  * ```ts
252
- * const { data: auth } = await verifyAuth(request, { allow: 'user' })
252
+ * const { data: auth } = await verifyAuth(request, { auth: 'user' })
253
253
  * const supabase = createContextClient({
254
254
  * auth: { token: auth.token, keyName: auth.keyName },
255
255
  * })
@@ -320,6 +320,37 @@ function extractCredentials(request) {
320
320
  };
321
321
  }
322
322
 
323
+ //#endregion
324
+ //#region src/core/utils/deprecation.ts
325
+ let allowDeprecationWarned = false;
326
+ /**
327
+ * Emits a one-time deprecation warning when the legacy `allow` option is used
328
+ * instead of `auth`. The warning fires at most once per process to avoid
329
+ * spamming logs in long-running servers.
330
+ *
331
+ * @internal
332
+ */
333
+ function warnAllowDeprecated() {
334
+ if (allowDeprecationWarned) return;
335
+ allowDeprecationWarned = true;
336
+ console.warn("[@supabase/server] The `allow` option is deprecated and will be removed in a future major release. Use `auth` instead — e.g. `{ auth: \"user\" }` instead of `{ allow: \"user\" }`.");
337
+ }
338
+ /**
339
+ * Resolves the auth mode from `auth` (preferred) or `allow` (deprecated),
340
+ * falling back to `"user"` when neither is provided. Emits a one-time
341
+ * deprecation warning when `allow` is used without `auth`.
342
+ *
343
+ * @internal
344
+ */
345
+ function resolveAuthOption(options) {
346
+ if (options.auth !== void 0) return options.auth;
347
+ if (options.allow !== void 0) {
348
+ warnAllowDeprecated();
349
+ return options.allow;
350
+ }
351
+ return "user";
352
+ }
353
+
323
354
  //#endregion
324
355
  //#region src/core/utils/timing-safe-equal.ts
325
356
  const encoder = new TextEncoder();
@@ -347,19 +378,19 @@ async function timingSafeEqual(a, b) {
347
378
  //#endregion
348
379
  //#region src/core/verify-credentials.ts
349
380
  /**
350
- * Parses an {@link AllowWithKey} string into its base mode and optional key name.
381
+ * Parses an {@link AuthModeWithKey} string into its base mode and optional key name.
351
382
  *
352
383
  * @example
353
384
  * ```
354
- * parseAllowMode('user') → { base: 'user', keyName: null }
355
- * parseAllowMode('public:web') → { base: 'public', keyName: 'web' }
356
- * parseAllowMode('secret:*') → { base: 'secret', keyName: '*' }
385
+ * parseAuthMode('user') → { base: 'user', keyName: null }
386
+ * parseAuthMode('publishable:web') → { base: 'publishable', keyName: 'web' }
387
+ * parseAuthMode('secret:*') → { base: 'secret', keyName: '*' }
357
388
  * ```
358
389
  *
359
390
  * @internal
360
391
  */
361
- function parseAllowMode(mode) {
362
- if (mode === "always" || mode === "public" || mode === "secret" || mode === "user") return {
392
+ function parseAuthMode(mode) {
393
+ if (mode === "none" || mode === "publishable" || mode === "secret" || mode === "user") return {
363
394
  base: mode,
364
395
  keyName: null
365
396
  };
@@ -379,13 +410,13 @@ function parseAllowMode(mode) {
379
410
  * Converts raw {@link JWTClaims} (snake_case) to a normalized {@link UserClaims} (camelCase).
380
411
  * @internal
381
412
  */
382
- function claimsToUserClaims(claims) {
413
+ function jwtClaimsToUserClaims(jwtClaims) {
383
414
  return {
384
- id: claims.sub,
385
- role: claims.role,
386
- email: claims.email,
387
- appMetadata: claims.app_metadata,
388
- userMetadata: claims.user_metadata
415
+ id: jwtClaims.sub,
416
+ role: jwtClaims.role,
417
+ email: jwtClaims.email,
418
+ appMetadata: jwtClaims.app_metadata,
419
+ userMetadata: jwtClaims.user_metadata
389
420
  };
390
421
  }
391
422
  const INVALID = Symbol("invalid");
@@ -400,34 +431,34 @@ const INVALID = Symbol("invalid");
400
431
  * @internal
401
432
  */
402
433
  async function tryMode(mode, credentials, env) {
403
- const { base, keyName } = parseAllowMode(mode);
434
+ const { base, keyName } = parseAuthMode(mode);
404
435
  switch (base) {
405
- case "always": return {
406
- authType: "always",
436
+ case "none": return {
437
+ authMode: "none",
407
438
  token: null,
408
439
  userClaims: null,
409
- claims: null,
440
+ jwtClaims: null,
410
441
  keyName: null
411
442
  };
412
- case "public": {
443
+ case "publishable": {
413
444
  if (!credentials.apikey) return null;
414
445
  const keys = env.publishableKeys;
415
446
  if (keyName === "*") {
416
447
  for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
417
- authType: "public",
448
+ authMode: "publishable",
418
449
  token: null,
419
450
  userClaims: null,
420
- claims: null,
451
+ jwtClaims: null,
421
452
  keyName: name
422
453
  };
423
454
  } else {
424
455
  const name = keyName ?? "default";
425
456
  const value = keys[name];
426
457
  if (value && await timingSafeEqual(credentials.apikey, value)) return {
427
- authType: "public",
458
+ authMode: "publishable",
428
459
  token: null,
429
460
  userClaims: null,
430
- claims: null,
461
+ jwtClaims: null,
431
462
  keyName: name
432
463
  };
433
464
  }
@@ -438,20 +469,20 @@ async function tryMode(mode, credentials, env) {
438
469
  const keys = env.secretKeys;
439
470
  if (keyName === "*") {
440
471
  for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
441
- authType: "secret",
472
+ authMode: "secret",
442
473
  token: null,
443
474
  userClaims: null,
444
- claims: null,
475
+ jwtClaims: null,
445
476
  keyName: name
446
477
  };
447
478
  } else {
448
479
  const name = keyName ?? "default";
449
480
  const value = keys[name];
450
481
  if (value && await timingSafeEqual(credentials.apikey, value)) return {
451
- authType: "secret",
482
+ authMode: "secret",
452
483
  token: null,
453
484
  userClaims: null,
454
- claims: null,
485
+ jwtClaims: null,
455
486
  keyName: name
456
487
  };
457
488
  }
@@ -464,12 +495,12 @@ async function tryMode(mode, credentials, env) {
464
495
  const jwkSet = (0, jose.createLocalJWKSet)(env.jwks);
465
496
  const { payload } = await (0, jose.jwtVerify)(credentials.token, jwkSet);
466
497
  if (typeof payload.sub !== "string") return INVALID;
467
- const claims = payload;
498
+ const jwtClaims = payload;
468
499
  return {
469
- authType: "user",
500
+ authMode: "user",
470
501
  token: credentials.token,
471
- userClaims: claimsToUserClaims(claims),
472
- claims,
502
+ userClaims: jwtClaimsToUserClaims(jwtClaims),
503
+ jwtClaims,
473
504
  keyName: null
474
505
  };
475
506
  } catch {
@@ -495,7 +526,7 @@ async function tryMode(mode, credentials, env) {
495
526
  * ```ts
496
527
  * const credentials = extractCredentials(request)
497
528
  * const { data: auth, error } = await verifyCredentials(credentials, {
498
- * allow: ['user', 'public'],
529
+ * auth: ['user', 'publishable'],
499
530
  * })
500
531
  * if (error) {
501
532
  * return Response.json({ message: error.message }, { status: error.status })
@@ -508,7 +539,8 @@ async function verifyCredentials(credentials, options) {
508
539
  data: null,
509
540
  error: new AuthError(envError.message, envError.code, 500)
510
541
  };
511
- const modes = Array.isArray(options.allow) ? options.allow : [options.allow];
542
+ const resolved = resolveAuthOption(options);
543
+ const modes = Array.isArray(resolved) ? resolved : [resolved];
512
544
  for (const mode of modes) {
513
545
  const result = await tryMode(mode, credentials, env);
514
546
  if (result === INVALID) return {
@@ -547,7 +579,7 @@ async function verifyCredentials(credentials, options) {
547
579
  * import { verifyAuth } from '@supabase/server/core'
548
580
  *
549
581
  * const { data: auth, error } = await verifyAuth(request, {
550
- * allow: 'user',
582
+ * auth: 'user',
551
583
  * })
552
584
  *
553
585
  * if (error) {
@@ -58,7 +58,7 @@ const EnvErrorMap = {
58
58
  * ```ts
59
59
  * import { AuthError, createSupabaseContext } from '@supabase/server'
60
60
  *
61
- * const { data: ctx, error } = await createSupabaseContext(request, { allow: 'user' })
61
+ * const { data: ctx, error } = await createSupabaseContext(request, { auth: 'user' })
62
62
  * if (error) {
63
63
  * // error is an AuthError
64
64
  * return Response.json(
@@ -249,7 +249,7 @@ function createAdminClient(options) {
249
249
  *
250
250
  * @example
251
251
  * ```ts
252
- * const { data: auth } = await verifyAuth(request, { allow: 'user' })
252
+ * const { data: auth } = await verifyAuth(request, { auth: 'user' })
253
253
  * const supabase = createContextClient({
254
254
  * auth: { token: auth.token, keyName: auth.keyName },
255
255
  * })
@@ -320,6 +320,37 @@ function extractCredentials(request) {
320
320
  };
321
321
  }
322
322
 
323
+ //#endregion
324
+ //#region src/core/utils/deprecation.ts
325
+ let allowDeprecationWarned = false;
326
+ /**
327
+ * Emits a one-time deprecation warning when the legacy `allow` option is used
328
+ * instead of `auth`. The warning fires at most once per process to avoid
329
+ * spamming logs in long-running servers.
330
+ *
331
+ * @internal
332
+ */
333
+ function warnAllowDeprecated() {
334
+ if (allowDeprecationWarned) return;
335
+ allowDeprecationWarned = true;
336
+ console.warn("[@supabase/server] The `allow` option is deprecated and will be removed in a future major release. Use `auth` instead — e.g. `{ auth: \"user\" }` instead of `{ allow: \"user\" }`.");
337
+ }
338
+ /**
339
+ * Resolves the auth mode from `auth` (preferred) or `allow` (deprecated),
340
+ * falling back to `"user"` when neither is provided. Emits a one-time
341
+ * deprecation warning when `allow` is used without `auth`.
342
+ *
343
+ * @internal
344
+ */
345
+ function resolveAuthOption(options) {
346
+ if (options.auth !== void 0) return options.auth;
347
+ if (options.allow !== void 0) {
348
+ warnAllowDeprecated();
349
+ return options.allow;
350
+ }
351
+ return "user";
352
+ }
353
+
323
354
  //#endregion
324
355
  //#region src/core/utils/timing-safe-equal.ts
325
356
  const encoder = new TextEncoder();
@@ -347,19 +378,19 @@ async function timingSafeEqual(a, b) {
347
378
  //#endregion
348
379
  //#region src/core/verify-credentials.ts
349
380
  /**
350
- * Parses an {@link AllowWithKey} string into its base mode and optional key name.
381
+ * Parses an {@link AuthModeWithKey} string into its base mode and optional key name.
351
382
  *
352
383
  * @example
353
384
  * ```
354
- * parseAllowMode('user') → { base: 'user', keyName: null }
355
- * parseAllowMode('public:web') → { base: 'public', keyName: 'web' }
356
- * parseAllowMode('secret:*') → { base: 'secret', keyName: '*' }
385
+ * parseAuthMode('user') → { base: 'user', keyName: null }
386
+ * parseAuthMode('publishable:web') → { base: 'publishable', keyName: 'web' }
387
+ * parseAuthMode('secret:*') → { base: 'secret', keyName: '*' }
357
388
  * ```
358
389
  *
359
390
  * @internal
360
391
  */
361
- function parseAllowMode(mode) {
362
- if (mode === "always" || mode === "public" || mode === "secret" || mode === "user") return {
392
+ function parseAuthMode(mode) {
393
+ if (mode === "none" || mode === "publishable" || mode === "secret" || mode === "user") return {
363
394
  base: mode,
364
395
  keyName: null
365
396
  };
@@ -379,13 +410,13 @@ function parseAllowMode(mode) {
379
410
  * Converts raw {@link JWTClaims} (snake_case) to a normalized {@link UserClaims} (camelCase).
380
411
  * @internal
381
412
  */
382
- function claimsToUserClaims(claims) {
413
+ function jwtClaimsToUserClaims(jwtClaims) {
383
414
  return {
384
- id: claims.sub,
385
- role: claims.role,
386
- email: claims.email,
387
- appMetadata: claims.app_metadata,
388
- userMetadata: claims.user_metadata
415
+ id: jwtClaims.sub,
416
+ role: jwtClaims.role,
417
+ email: jwtClaims.email,
418
+ appMetadata: jwtClaims.app_metadata,
419
+ userMetadata: jwtClaims.user_metadata
389
420
  };
390
421
  }
391
422
  const INVALID = Symbol("invalid");
@@ -400,34 +431,34 @@ const INVALID = Symbol("invalid");
400
431
  * @internal
401
432
  */
402
433
  async function tryMode(mode, credentials, env) {
403
- const { base, keyName } = parseAllowMode(mode);
434
+ const { base, keyName } = parseAuthMode(mode);
404
435
  switch (base) {
405
- case "always": return {
406
- authType: "always",
436
+ case "none": return {
437
+ authMode: "none",
407
438
  token: null,
408
439
  userClaims: null,
409
- claims: null,
440
+ jwtClaims: null,
410
441
  keyName: null
411
442
  };
412
- case "public": {
443
+ case "publishable": {
413
444
  if (!credentials.apikey) return null;
414
445
  const keys = env.publishableKeys;
415
446
  if (keyName === "*") {
416
447
  for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
417
- authType: "public",
448
+ authMode: "publishable",
418
449
  token: null,
419
450
  userClaims: null,
420
- claims: null,
451
+ jwtClaims: null,
421
452
  keyName: name
422
453
  };
423
454
  } else {
424
455
  const name = keyName ?? "default";
425
456
  const value = keys[name];
426
457
  if (value && await timingSafeEqual(credentials.apikey, value)) return {
427
- authType: "public",
458
+ authMode: "publishable",
428
459
  token: null,
429
460
  userClaims: null,
430
- claims: null,
461
+ jwtClaims: null,
431
462
  keyName: name
432
463
  };
433
464
  }
@@ -438,20 +469,20 @@ async function tryMode(mode, credentials, env) {
438
469
  const keys = env.secretKeys;
439
470
  if (keyName === "*") {
440
471
  for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
441
- authType: "secret",
472
+ authMode: "secret",
442
473
  token: null,
443
474
  userClaims: null,
444
- claims: null,
475
+ jwtClaims: null,
445
476
  keyName: name
446
477
  };
447
478
  } else {
448
479
  const name = keyName ?? "default";
449
480
  const value = keys[name];
450
481
  if (value && await timingSafeEqual(credentials.apikey, value)) return {
451
- authType: "secret",
482
+ authMode: "secret",
452
483
  token: null,
453
484
  userClaims: null,
454
- claims: null,
485
+ jwtClaims: null,
455
486
  keyName: name
456
487
  };
457
488
  }
@@ -464,12 +495,12 @@ async function tryMode(mode, credentials, env) {
464
495
  const jwkSet = createLocalJWKSet(env.jwks);
465
496
  const { payload } = await jwtVerify(credentials.token, jwkSet);
466
497
  if (typeof payload.sub !== "string") return INVALID;
467
- const claims = payload;
498
+ const jwtClaims = payload;
468
499
  return {
469
- authType: "user",
500
+ authMode: "user",
470
501
  token: credentials.token,
471
- userClaims: claimsToUserClaims(claims),
472
- claims,
502
+ userClaims: jwtClaimsToUserClaims(jwtClaims),
503
+ jwtClaims,
473
504
  keyName: null
474
505
  };
475
506
  } catch {
@@ -495,7 +526,7 @@ async function tryMode(mode, credentials, env) {
495
526
  * ```ts
496
527
  * const credentials = extractCredentials(request)
497
528
  * const { data: auth, error } = await verifyCredentials(credentials, {
498
- * allow: ['user', 'public'],
529
+ * auth: ['user', 'publishable'],
499
530
  * })
500
531
  * if (error) {
501
532
  * return Response.json({ message: error.message }, { status: error.status })
@@ -508,7 +539,8 @@ async function verifyCredentials(credentials, options) {
508
539
  data: null,
509
540
  error: new AuthError(envError.message, envError.code, 500)
510
541
  };
511
- const modes = Array.isArray(options.allow) ? options.allow : [options.allow];
542
+ const resolved = resolveAuthOption(options);
543
+ const modes = Array.isArray(resolved) ? resolved : [resolved];
512
544
  for (const mode of modes) {
513
545
  const result = await tryMode(mode, credentials, env);
514
546
  if (result === INVALID) return {
@@ -547,7 +579,7 @@ async function verifyCredentials(credentials, options) {
547
579
  * import { verifyAuth } from '@supabase/server/core'
548
580
  *
549
581
  * const { data: auth, error } = await verifyAuth(request, {
550
- * allow: 'user',
582
+ * auth: 'user',
551
583
  * })
552
584
  *
553
585
  * if (error) {
@@ -0,0 +1,180 @@
1
+ # H3 / Nuxt Adapter
2
+
3
+ ## Setup
4
+
5
+ Install H3 as a peer dependency:
6
+
7
+ ```bash
8
+ pnpm add h3
9
+ ```
10
+
11
+ The adapter exports its own `withSupabase` that returns H3 middleware instead of a fetch handler. Works with standalone H3 servers and Nuxt server routes (which run on H3 under the hood).
12
+
13
+ ## Basic app with auth
14
+
15
+ ```ts
16
+ import { H3 } from 'h3'
17
+ import { withSupabase } from '@supabase/server/adapters/h3'
18
+
19
+ const app = new H3()
20
+
21
+ // Apply auth to all routes
22
+ app.use(withSupabase({ auth: 'user' }))
23
+
24
+ app.get('/todos', async (event) => {
25
+ const { supabase } = event.context.supabaseContext
26
+ const { data } = await supabase.from('todos').select()
27
+ return data
28
+ })
29
+
30
+ app.get('/profile', async (event) => {
31
+ const { supabase, userClaims } = event.context.supabaseContext
32
+ const { data } = await supabase
33
+ .from('profiles')
34
+ .select()
35
+ .eq('id', userClaims!.id)
36
+ return data
37
+ })
38
+
39
+ export default { fetch: app.fetch }
40
+ ```
41
+
42
+ The context is stored in `event.context.supabaseContext` and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, and `authMode`.
43
+
44
+ ## Per-route auth
45
+
46
+ Apply different auth modes to different routes by attaching the middleware to specific handlers:
47
+
48
+ ```ts
49
+ import { H3 } from 'h3'
50
+ import { withSupabase } from '@supabase/server/adapters/h3'
51
+
52
+ const app = new H3()
53
+
54
+ // Public route — no auth
55
+ app.get('/health', () => ({ status: 'ok' }))
56
+
57
+ // User-authenticated route
58
+ app.get('/todos', withSupabase({ auth: 'user' }), async (event) => {
59
+ const { supabase } = event.context.supabaseContext
60
+ const { data } = await supabase.from('todos').select()
61
+ return data
62
+ })
63
+
64
+ // Secret-key-protected admin route
65
+ app.post('/admin/sync', withSupabase({ auth: 'secret' }), async (event) => {
66
+ const { supabaseAdmin } = event.context.supabaseContext
67
+ const { data } = await supabaseAdmin
68
+ .from('audit_log')
69
+ .insert({ action: 'sync' })
70
+ return data
71
+ })
72
+
73
+ // Dual auth — users or services
74
+ app.get(
75
+ '/reports',
76
+ withSupabase({ auth: ['user', 'secret'] }),
77
+ async (event) => {
78
+ const { authMode } = event.context.supabaseContext
79
+ return { authMode }
80
+ },
81
+ )
82
+
83
+ export default { fetch: app.fetch }
84
+ ```
85
+
86
+ ## Nuxt: file-based routes
87
+
88
+ Use `defineHandler` to attach auth to a single Nuxt server route:
89
+
90
+ ```ts
91
+ // server/api/games.get.ts
92
+ import { defineHandler } from 'h3'
93
+ import { withSupabase } from '@supabase/server/adapters/h3'
94
+
95
+ export default defineHandler({
96
+ middleware: [withSupabase({ auth: 'user' })],
97
+ handler: async (event) => {
98
+ const { supabase } = event.context.supabaseContext
99
+ return supabase.from('favorite_games').select()
100
+ },
101
+ })
102
+ ```
103
+
104
+ ## Nuxt: app-wide auth
105
+
106
+ Register as a server middleware to apply auth to every route:
107
+
108
+ ```ts
109
+ // server/middleware/supabase.ts
110
+ import { withSupabase } from '@supabase/server/adapters/h3'
111
+
112
+ export default withSupabase({ auth: 'user' })
113
+ ```
114
+
115
+ ## Skip behavior
116
+
117
+ If a previous middleware already set `event.context.supabaseContext`, subsequent `withSupabase` calls skip auth. This enables a pattern where route-level middleware overrides the app-wide default:
118
+
119
+ ```ts
120
+ const app = new H3()
121
+
122
+ // App-wide: require user auth
123
+ app.use(withSupabase({ auth: 'user' }))
124
+
125
+ // This route needs secret auth instead.
126
+ // The route-level middleware runs first, sets the context,
127
+ // and the app-wide middleware skips.
128
+ app.post('/webhook', withSupabase({ auth: 'secret' }), async (event) => {
129
+ const { supabaseAdmin } = event.context.supabaseContext
130
+ // ...
131
+ })
132
+ ```
133
+
134
+ ## CORS
135
+
136
+ The H3 adapter does not handle CORS — the `cors` option is excluded from its config type. Use H3's built-in CORS utilities:
137
+
138
+ ```ts
139
+ import { H3, handleCors } from 'h3'
140
+ import { withSupabase } from '@supabase/server/adapters/h3'
141
+
142
+ const app = new H3()
143
+
144
+ app.use((event) => handleCors(event, { origin: '*' }))
145
+ app.use(withSupabase({ auth: 'user' }))
146
+
147
+ app.get('/todos', async (event) => {
148
+ const { supabase } = event.context.supabaseContext
149
+ const { data } = await supabase.from('todos').select()
150
+ return data
151
+ })
152
+
153
+ export default { fetch: app.fetch }
154
+ ```
155
+
156
+ ## Environment overrides
157
+
158
+ Pass `env` to override auto-detected environment variables, same as the main wrapper:
159
+
160
+ ```ts
161
+ app.use(
162
+ withSupabase({
163
+ auth: 'user',
164
+ env: { url: 'http://localhost:54321' },
165
+ }),
166
+ )
167
+ ```
168
+
169
+ ## Supabase client options
170
+
171
+ Forward options to the underlying `createClient()` calls:
172
+
173
+ ```ts
174
+ app.use(
175
+ withSupabase({
176
+ auth: 'user',
177
+ supabaseOptions: { db: { schema: 'api' } },
178
+ }),
179
+ )
180
+ ```