@supabase/server 0.2.0 → 1.0.0-rc.53

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
@@ -19,7 +19,7 @@ import { withSupabase } from '@supabase/server/adapters/hono'
19
19
  const app = new Hono()
20
20
 
21
21
  // Apply auth to all routes
22
- app.use('*', withSupabase({ allow: 'user' }))
22
+ app.use('*', withSupabase({ auth: 'user' }))
23
23
 
24
24
  app.get('/todos', async (c) => {
25
25
  const { supabase } = c.var.supabaseContext
@@ -39,7 +39,7 @@ app.get('/profile', async (c) => {
39
39
  export default { fetch: app.fetch }
40
40
  ```
41
41
 
42
- The context is stored in `c.var.supabaseContext` and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `claims`, and `authType`.
42
+ The context is stored in `c.var.supabaseContext` and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, and `authMode`.
43
43
 
44
44
  ## Per-route auth
45
45
 
@@ -55,14 +55,14 @@ const app = new Hono()
55
55
  app.get('/health', (c) => c.json({ status: 'ok' }))
56
56
 
57
57
  // User-authenticated route
58
- app.get('/todos', withSupabase({ allow: 'user' }), async (c) => {
58
+ app.get('/todos', withSupabase({ auth: 'user' }), async (c) => {
59
59
  const { supabase } = c.var.supabaseContext
60
60
  const { data } = await supabase.from('todos').select()
61
61
  return c.json(data)
62
62
  })
63
63
 
64
64
  // Secret-key-protected admin route
65
- app.post('/admin/sync', withSupabase({ allow: 'secret' }), async (c) => {
65
+ app.post('/admin/sync', withSupabase({ auth: 'secret' }), async (c) => {
66
66
  const { supabaseAdmin } = c.var.supabaseContext
67
67
  const { data } = await supabaseAdmin
68
68
  .from('audit_log')
@@ -71,9 +71,9 @@ app.post('/admin/sync', withSupabase({ allow: 'secret' }), async (c) => {
71
71
  })
72
72
 
73
73
  // Dual auth — users or services
74
- app.get('/reports', withSupabase({ allow: ['user', 'secret'] }), async (c) => {
75
- const { supabase, authType } = c.var.supabaseContext
76
- return c.json({ authType })
74
+ app.get('/reports', withSupabase({ auth: ['user', 'secret'] }), async (c) => {
75
+ const { supabase, authMode } = c.var.supabaseContext
76
+ return c.json({ authMode })
77
77
  })
78
78
 
79
79
  export default { fetch: app.fetch }
@@ -81,22 +81,11 @@ export default { fetch: app.fetch }
81
81
 
82
82
  ## Skip behavior
83
83
 
84
- If a previous middleware already set `c.var.supabaseContext`, subsequent `withSupabase` calls skip auth. This enables a pattern where route-level middleware overrides the app-wide default:
84
+ If a previous middleware already set `c.var.supabaseContext`, subsequent `withSupabase` calls skip auth. This matters when multiple `app.use` middlewares overlap on the same path — the first one to set the context wins.
85
85
 
86
- ```ts
87
- const app = new Hono()
88
-
89
- // App-wide: require user auth
90
- app.use('*', withSupabase({ allow: 'user' }))
86
+ **Important:** Hono runs middleware in registration order (`app.use` before route-level middleware). An `app.use('*', ...)` middleware will always run before inline route middleware, so the skip-if-set pattern cannot be used to make a route stricter than the app-wide default.
91
87
 
92
- // This route needs secret auth instead.
93
- // The route-level middleware runs first, sets the context,
94
- // and the app-wide middleware skips.
95
- app.post('/webhook', withSupabase({ allow: 'secret' }), async (c) => {
96
- const { supabaseAdmin } = c.var.supabaseContext
97
- // ...
98
- })
99
- ```
88
+ For routes that need different auth than the rest of the app, use per-route middleware without an app-wide middleware (see the "Per-route auth" section above).
100
89
 
101
90
  ## CORS
102
91
 
@@ -110,7 +99,7 @@ import { withSupabase } from '@supabase/server/adapters/hono'
110
99
  const app = new Hono()
111
100
 
112
101
  app.use('*', cors())
113
- app.use('*', withSupabase({ allow: 'user' }))
102
+ app.use('*', withSupabase({ auth: 'user' }))
114
103
 
115
104
  app.get('/todos', async (c) => {
116
105
  const { supabase } = c.var.supabaseContext
@@ -133,7 +122,7 @@ import { AuthError } from '@supabase/server'
133
122
 
134
123
  const app = new Hono()
135
124
 
136
- app.use('*', withSupabase({ allow: 'user' }))
125
+ app.use('*', withSupabase({ auth: 'user' }))
137
126
 
138
127
  // Custom error handler
139
128
  app.onError((err, c) => {
@@ -164,7 +153,7 @@ Pass `env` to override auto-detected environment variables, same as the main wra
164
153
  app.use(
165
154
  '*',
166
155
  withSupabase({
167
- allow: 'user',
156
+ auth: 'user',
168
157
  env: { url: 'http://localhost:54321' },
169
158
  }),
170
159
  )
@@ -178,7 +167,7 @@ Forward options to the underlying `createClient()` calls:
178
167
  app.use(
179
168
  '*',
180
169
  withSupabase({
181
- allow: 'user',
170
+ auth: 'user',
182
171
  supabaseOptions: { db: { schema: 'api' } },
183
172
  }),
184
173
  )
@@ -18,7 +18,7 @@ function withSupabase<Database = unknown>(
18
18
  Wraps a fetch handler with auth, CORS, and client creation. Returns a `(req: Request) => Promise<Response>` function suitable for `export default { fetch }`.
19
19
 
20
20
  - Handles `OPTIONS` preflight when CORS is enabled
21
- - Verifies credentials per `config.allow`
21
+ - Verifies credentials per `config.auth`
22
22
  - Returns JSON error response on auth failure
23
23
  - Adds CORS headers to all responses
24
24
 
@@ -36,7 +36,7 @@ function createSupabaseContext<Database = unknown>(
36
36
 
37
37
  Creates a `SupabaseContext` from a request. Returns a result tuple. The `cors` option is ignored.
38
38
 
39
- Defaults to `allow: 'user'` when `options` is omitted.
39
+ Defaults to `auth: 'user'` when `options` is omitted.
40
40
 
41
41
  ---
42
42
 
@@ -47,7 +47,10 @@ Defaults to `allow: 'user'` when `options` is omitted.
47
47
  ```ts
48
48
  function verifyAuth(
49
49
  request: Request,
50
- options: { allow: AllowWithKey | AllowWithKey[]; env?: Partial<SupabaseEnv> },
50
+ options: {
51
+ auth?: AuthModeWithKey | AuthModeWithKey[]
52
+ env?: Partial<SupabaseEnv>
53
+ },
51
54
  ): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
52
55
  ```
53
56
 
@@ -58,7 +61,10 @@ Extracts credentials from a request and verifies them. Convenience wrapper over
58
61
  ```ts
59
62
  function verifyCredentials(
60
63
  credentials: Credentials,
61
- options: { allow: AllowWithKey | AllowWithKey[]; env?: Partial<SupabaseEnv> },
64
+ options: {
65
+ auth?: AuthModeWithKey | AuthModeWithKey[]
66
+ env?: Partial<SupabaseEnv>
67
+ },
62
68
  ): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
63
69
  ```
64
70
 
@@ -124,25 +130,29 @@ Hono middleware. Sets `c.var.supabaseContext` on the Hono context. Throws `HTTPE
124
130
 
125
131
  Skips if `c.var.supabaseContext` is already set (enables route-level overrides).
126
132
 
127
- Defaults to `allow: 'user'` when config is omitted.
133
+ Defaults to `auth: 'user'` when config is omitted.
128
134
 
129
135
  ---
130
136
 
131
137
  ## Types
132
138
 
133
- ### Allow
139
+ ### AuthMode
134
140
 
135
141
  ```ts
136
- type Allow = 'always' | 'public' | 'secret' | 'user'
142
+ type AuthMode = 'none' | 'publishable' | 'secret' | 'user'
137
143
  ```
138
144
 
139
- ### AllowWithKey
145
+ ### AuthModeWithKey
140
146
 
141
147
  ```ts
142
- type AllowWithKey = Allow | `public:${string}` | `secret:${string}`
148
+ type AuthModeWithKey = AuthMode | `publishable:${string}` | `secret:${string}`
143
149
  ```
144
150
 
145
- Extended auth mode with named key support. Examples: `'public:web'`, `'secret:*'`, `'secret:internal'`.
151
+ Extended auth mode with named key support. Examples: `'publishable:web'`, `'secret:*'`, `'secret:internal'`.
152
+
153
+ ### Allow / AllowWithKey (deprecated aliases)
154
+
155
+ `Allow` and `AllowWithKey` are kept as deprecated aliases for `AuthMode` and `AuthModeWithKey`. Prefer the `Auth*` names — the legacy ones will be removed in a future major release.
146
156
 
147
157
  ### SupabaseContext\<Database\>
148
158
 
@@ -151,8 +161,9 @@ interface SupabaseContext<Database = unknown> {
151
161
  supabase: SupabaseClient<Database>
152
162
  supabaseAdmin: SupabaseClient<Database>
153
163
  userClaims: UserClaims | null
154
- claims: JWTClaims | null
155
- authType: Allow
164
+ jwtClaims: JWTClaims | null
165
+ authMode: AuthMode
166
+ authKeyName?: string
156
167
  }
157
168
  ```
158
169
 
@@ -160,7 +171,9 @@ interface SupabaseContext<Database = unknown> {
160
171
 
161
172
  ```ts
162
173
  interface WithSupabaseConfig {
163
- allow?: AllowWithKey | AllowWithKey[] // default: 'user'
174
+ auth?: AuthModeWithKey | AuthModeWithKey[] // default: 'user'
175
+ /** @deprecated use `auth` instead — will be removed in a future major release */
176
+ allow?: AuthModeWithKey | AuthModeWithKey[]
164
177
  env?: Partial<SupabaseEnv>
165
178
  cors?: boolean | Record<string, string> // default: true
166
179
  supabaseOptions?: SupabaseClientOptions<string>
@@ -191,10 +204,10 @@ interface Credentials {
191
204
 
192
205
  ```ts
193
206
  interface AuthResult {
194
- authType: Allow
207
+ authMode: AuthMode
195
208
  token: string | null
196
209
  userClaims: UserClaims | null
197
- claims: JWTClaims | null
210
+ jwtClaims: JWTClaims | null
198
211
  keyName?: string | null
199
212
  }
200
213
  ```
@@ -2,17 +2,21 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- Every request is validated against one or more auth modes before your handler runs. The `allow` config determines which modes are accepted.
5
+ Every request is validated against one or more auth modes before your handler runs. The `auth` config determines which modes are accepted.
6
6
 
7
- | Mode | Credential required | Typical use case |
8
- | ---------- | -------------------------------------------- | -------------------------------------- |
9
- | `'user'` | Valid JWT in `Authorization: Bearer <token>` | Authenticated user endpoints |
10
- | `'public'` | Valid publishable key in `apikey` header | Client-facing, key-validated endpoints |
11
- | `'secret'` | Valid secret key in `apikey` header | Server-to-server, internal calls |
12
- | `'always'` | None | Open endpoints, custom auth wrappers |
7
+ > **`allow` is deprecated.** The `auth` option replaces the legacy `allow` option. `allow` still works (with a one-time `console.warn`) but will be removed in a future major release. Migration is a find-and-replace: `allow:` → `auth:`.
8
+
9
+ > **Breaking — auth API renamed.** `'always'` is now `'none'` and `'public'` is now `'publishable'` (including the colon variants `'public:<name>'` → `'publishable:<name>'`). The field on `AuthResult` and `SupabaseContext` was also renamed from `authType` to `authMode` so it matches the `AuthMode` type. The old names no longer work — update the option values you pass in **and** any runtime checks on `ctx.authType` (now `ctx.authMode`).
10
+
11
+ | Mode | Credential required | Typical use case |
12
+ | --------------- | -------------------------------------------- | -------------------------------------- |
13
+ | `'user'` | Valid JWT in `Authorization: Bearer <token>` | Authenticated user endpoints |
14
+ | `'publishable'` | Valid publishable key in `apikey` header | Client-facing, key-validated endpoints |
15
+ | `'secret'` | Valid secret key in `apikey` header | Server-to-server, internal calls |
16
+ | `'none'` | None | Open endpoints, custom auth wrappers |
13
17
 
14
18
  > **Supabase Edge Functions:** By default, the platform requires a valid JWT on every request same as `'user'`.
15
- > If your function uses `'public'`, `'secret'` or `'always'`, disable the platform-level JWT check in `supabase/config.toml`:
19
+ > If your function uses `'publishable'`, `'secret'` or `'none'`, disable the platform-level JWT check in `supabase/config.toml`:
16
20
  >
17
21
  > ```toml
18
22
  > [functions.my-function]
@@ -27,15 +31,15 @@ The default. Verifies the JWT using your project's JWKS (JSON Web Key Set).
27
31
  import { withSupabase } from '@supabase/server'
28
32
 
29
33
  export default {
30
- fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
34
+ fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
31
35
  // ctx.userClaims has the caller's identity
32
36
  console.log(ctx.userClaims!.id) // "d0f1a2b3-..."
33
37
  console.log(ctx.userClaims!.email) // "user@example.com"
34
38
  console.log(ctx.userClaims!.role) // "authenticated"
35
39
 
36
- // ctx.claims has the raw JWT payload
37
- console.log(ctx.claims!.sub) // same as userClaims.id
38
- console.log(ctx.claims!.exp) // token expiration (epoch seconds)
40
+ // ctx.jwtClaims has the raw JWT payload
41
+ console.log(ctx.jwtClaims!.sub) // same as userClaims.id
42
+ console.log(ctx.jwtClaims!.exp) // token expiration (epoch seconds)
39
43
 
40
44
  // ctx.supabase is scoped to this user — RLS applies
41
45
  const { data } = await ctx.supabase.from('todos').select()
@@ -52,7 +56,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
52
56
 
53
57
  **`userClaims` vs `supabase.auth.getUser()`:** `userClaims` is extracted from the JWT and is available instantly — no network call. It includes `id`, `email`, `role`, `appMetadata`, and `userMetadata`. For the full Supabase `User` object (email confirmation status, providers, linked identities), call `ctx.supabase.auth.getUser()`, which makes a request to the auth server.
54
58
 
55
- ## Public mode
59
+ ## Publishable mode
56
60
 
57
61
  Validates that the `apikey` header contains a recognized publishable key. Uses timing-safe comparison to prevent timing attacks. See [`security.md`](security.md) for details.
58
62
 
@@ -60,7 +64,7 @@ Validates that the `apikey` header contains a recognized publishable key. Uses t
60
64
  import { withSupabase } from '@supabase/server'
61
65
 
62
66
  export default {
63
- fetch: withSupabase({ allow: 'public' }, async (_req, ctx) => {
67
+ fetch: withSupabase({ auth: 'publishable' }, async (_req, ctx) => {
64
68
  // ctx.userClaims is null — no JWT involved
65
69
  // ctx.supabase is initialized as anonymous (RLS anon role)
66
70
  const { data } = await ctx.supabase.from('products').select()
@@ -75,17 +79,17 @@ The caller must send:
75
79
  apikey: sb_publishable_abc123...
76
80
  ```
77
81
 
78
- By default, `public` mode validates against the `"default"` key in `SUPABASE_PUBLISHABLE_KEYS`. Use named key syntax to target a specific key (see below).
82
+ By default, `publishable` mode validates against the `"default"` key in `SUPABASE_PUBLISHABLE_KEYS`. Use named key syntax to target a specific key (see below).
79
83
 
80
84
  ## Secret mode
81
85
 
82
- Validates that the `apikey` header contains a recognized secret key. Same timing-safe comparison as public mode. See [`security.md`](security.md) for details.
86
+ Validates that the `apikey` header contains a recognized secret key. Same timing-safe comparison as publishable mode. See [`security.md`](security.md) for details.
83
87
 
84
88
  ```ts
85
89
  import { withSupabase } from '@supabase/server'
86
90
 
87
91
  export default {
88
- fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
92
+ fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => {
89
93
  // ctx.supabaseAdmin bypasses RLS — use for privileged operations
90
94
  const { data } = await ctx.supabaseAdmin.from('config').select()
91
95
  return Response.json(data)
@@ -99,7 +103,7 @@ The caller must send:
99
103
  apikey: sb_secret_xyz789...
100
104
  ```
101
105
 
102
- ## Always mode
106
+ ## None mode
103
107
 
104
108
  No credentials required. Every request is accepted.
105
109
 
@@ -107,8 +111,8 @@ No credentials required. Every request is accepted.
107
111
  import { withSupabase } from '@supabase/server'
108
112
 
109
113
  export default {
110
- fetch: withSupabase({ allow: 'always' }, async (_req, ctx) => {
111
- // ctx.authType is 'always'
114
+ fetch: withSupabase({ auth: 'none' }, async (_req, ctx) => {
115
+ // ctx.authMode is 'none'
112
116
  // ctx.userClaims is null
113
117
  // ctx.supabase is anonymous (RLS anon role)
114
118
  return Response.json({ status: 'healthy' })
@@ -116,7 +120,7 @@ export default {
116
120
  }
117
121
  ```
118
122
 
119
- Use `always` for health checks, public APIs, or when you handle auth yourself inside the handler.
123
+ Use `none` for health checks, public APIs, or when you handle auth yourself inside the handler.
120
124
 
121
125
  ## Array syntax (multiple modes)
122
126
 
@@ -126,9 +130,9 @@ Accept multiple auth methods. Modes are tried in order — the first match wins.
126
130
  import { withSupabase } from '@supabase/server'
127
131
 
128
132
  export default {
129
- fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
130
- // ctx.authType tells you which mode matched
131
- if (ctx.authType === 'user') {
133
+ fetch: withSupabase({ auth: ['user', 'secret'] }, async (req, ctx) => {
134
+ // ctx.authMode tells you which mode matched
135
+ if (ctx.authMode === 'user') {
132
136
  // Called by an authenticated user
133
137
  const { data } = await ctx.supabase.from('reports').select()
134
138
  return Response.json(data)
@@ -147,7 +151,7 @@ export default {
147
151
 
148
152
  A request with a valid JWT matches `'user'`. A request with a valid secret key matches `'secret'`. A request with neither is rejected.
149
153
 
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.
154
+ **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 `'publishable'`, `'secret'`, or `'none'`. The same rule applies on the API-key side: `'publishable'` 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
155
 
152
156
  ## Named key syntax
153
157
 
@@ -167,39 +171,39 @@ Keys are stored as a JSON object in `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SEC
167
171
 
168
172
  ```ts
169
173
  // Only accept the "web" publishable key
170
- withSupabase({ allow: 'public:web' }, handler)
174
+ withSupabase({ auth: 'publishable:web' }, handler)
171
175
 
172
176
  // Only accept the "internal" secret key
173
- withSupabase({ allow: 'secret:internal' }, handler)
177
+ withSupabase({ auth: 'secret:internal' }, handler)
174
178
  ```
175
179
 
176
180
  ### Wildcard — accept any key in the set
177
181
 
178
182
  ```ts
179
183
  // Accept any publishable key
180
- withSupabase({ allow: 'public:*' }, handler)
184
+ withSupabase({ auth: 'publishable:*' }, handler)
181
185
 
182
186
  // Accept any secret key
183
- withSupabase({ allow: 'secret:*' }, handler)
187
+ withSupabase({ auth: 'secret:*' }, handler)
184
188
  ```
185
189
 
186
190
  ### Which key matched?
187
191
 
188
- When using named keys, `ctx.authType` tells you the mode and `keyName` on the `AuthResult` (from core primitives) tells you which key matched. In the high-level `withSupabase` wrapper, the matched key is used internally for client creation.
192
+ When using named keys, `ctx.authMode` tells you the mode and `keyName` on the `AuthResult` (from core primitives) tells you which key matched. In the high-level `withSupabase` wrapper, the matched key is used internally for client creation.
189
193
 
190
194
  ### Combining named keys with other modes
191
195
 
192
196
  ```ts
193
- withSupabase({ allow: ['user', 'public:web'] }, async (_req, ctx) => {
197
+ withSupabase({ auth: ['user', 'publishable:web'] }, async (_req, ctx) => {
194
198
  // Accepts either a valid JWT or the "web" publishable key
195
- return Response.json({ authType: ctx.authType })
199
+ return Response.json({ authMode: ctx.authMode })
196
200
  })
197
201
  ```
198
202
 
199
203
  ## How auth flows through the system
200
204
 
201
205
  1. `extractCredentials(request)` reads `Authorization: Bearer <token>` and `apikey` from headers
202
- 2. Each mode in `allow` is tried in order against the extracted credentials
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`.
206
+ 2. Each mode in `auth` is tried in order against the extracted credentials
207
+ 3. First match wins — returns an `AuthResult` with `authMode`, `token`, `userClaims`, `jwtClaims`, 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`.
204
208
  4. The auth result is used to create scoped clients (`supabase` with the user's token, `supabaseAdmin` with the secret key)
205
209
  5. Everything is bundled into a `SupabaseContext` and passed to your handler
@@ -19,7 +19,7 @@ The primitives compose into a pipeline. Each step is independent — use only wh
19
19
  ```
20
20
  resolveEnv() → SupabaseEnv
21
21
  extractCredentials(request) → Credentials { token, apikey }
22
- verifyCredentials(credentials, opts) → AuthResult { authType, token, userClaims, claims, keyName }
22
+ verifyCredentials(credentials, opts) → AuthResult { authMode, token, userClaims, jwtClaims, keyName }
23
23
  createContextClient(options) → SupabaseClient (RLS-scoped)
24
24
  createAdminClient(options) → SupabaseClient (bypasses RLS)
25
25
  ```
@@ -77,14 +77,14 @@ import { verifyCredentials } from '@supabase/server/core'
77
77
 
78
78
  const credentials = { token: cookieToken, apikey: null }
79
79
  const { data: auth, error } = await verifyCredentials(credentials, {
80
- allow: 'user',
80
+ auth: 'user',
81
81
  })
82
82
 
83
83
  if (error) {
84
84
  return Response.json({ message: error.message }, { status: error.status })
85
85
  }
86
86
 
87
- console.log(auth!.authType) // 'user'
87
+ console.log(auth!.authMode) // 'user'
88
88
  console.log(auth!.userClaims) // { id: '...', email: '...', role: 'authenticated' }
89
89
  ```
90
90
 
@@ -93,17 +93,17 @@ Supports all auth mode syntax — single mode, arrays, and named keys:
93
93
  ```ts
94
94
  // Multiple modes
95
95
  const { data: auth } = await verifyCredentials(creds, {
96
- allow: ['user', 'public'],
96
+ auth: ['user', 'publishable'],
97
97
  })
98
98
 
99
99
  // Named key
100
100
  const { data: auth } = await verifyCredentials(creds, {
101
- allow: 'public:web',
101
+ auth: 'publishable:web',
102
102
  })
103
103
 
104
104
  // Wildcard
105
105
  const { data: auth } = await verifyCredentials(creds, {
106
- allow: 'secret:*',
106
+ auth: 'secret:*',
107
107
  })
108
108
  ```
109
109
 
@@ -115,7 +115,7 @@ Convenience function that combines `extractCredentials` and `verifyCredentials`
115
115
  import { verifyAuth } from '@supabase/server/core'
116
116
 
117
117
  const { data: auth, error } = await verifyAuth(request, {
118
- allow: 'user',
118
+ auth: 'user',
119
119
  })
120
120
 
121
121
  if (error) {
@@ -134,7 +134,7 @@ Creates a Supabase client scoped to the caller's identity. RLS policies apply.
134
134
  import { verifyAuth, createContextClient } from '@supabase/server/core'
135
135
 
136
136
  // With a user's token (from verifyAuth)
137
- const { data: auth } = await verifyAuth(request, { allow: 'user' })
137
+ const { data: auth } = await verifyAuth(request, { auth: 'user' })
138
138
  const supabase = createContextClient({
139
139
  auth: { token: auth!.token, keyName: auth!.keyName },
140
140
  })
@@ -194,7 +194,7 @@ export default {
194
194
 
195
195
  // User-authenticated route
196
196
  if (url.pathname === '/todos') {
197
- const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
197
+ const { data: auth, error } = await verifyAuth(req, { auth: 'user' })
198
198
  if (error) {
199
199
  return Response.json(
200
200
  { message: error.message },
@@ -212,7 +212,7 @@ export default {
212
212
  // Admin route — secret key only
213
213
  if (url.pathname === '/admin/users') {
214
214
  const { data: auth, error } = await verifyAuth(req, {
215
- allow: 'secret',
215
+ auth: 'secret',
216
216
  })
217
217
  if (error) {
218
218
  return Response.json(
@@ -233,8 +233,8 @@ export default {
233
233
  }
234
234
  ```
235
235
 
236
- ## SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)
236
+ ## Cookie-based environments (with `@supabase/ssr`)
237
237
 
238
- In SSR frameworks, the JWT lives in session cookies rather than the `Authorization` header. Use `verifyCredentials` with a token extracted from cookies, then create clients as usual. This is the key primitive that enables SSR integration it accepts pre-extracted credentials from any source.
238
+ In Next.js, SvelteKit, Remix, and other cookie-based frameworks, the JWT lives in session cookies rather than the `Authorization` header. The recommended pattern is to **compose with [`@supabase/ssr`](https://github.com/supabase/ssr)**: let `@supabase/ssr` own the cookie session lifecycle and refresh-token rotation (via middleware), then hand its fresh access token to `verifyCredentials` and build typed clients with `createContextClient` + `createAdminClient`.
239
239
 
240
- For a complete guide with cookie parsing, JWKS caching, env bridging, and full framework adapters, see [ssr-frameworks.md](ssr-frameworks.md).
240
+ For the full pattern middleware setup, the composed adapter, JWKS caching, and other-framework adapting tips — see [ssr-frameworks.md](ssr-frameworks.md).
@@ -5,22 +5,22 @@ On Supabase Platform and Local Development (CLI), all variables are auto-provisi
5
5
  | Variable | Format | Description | Available in |
6
6
  | --------------------------- | ---------------------------------- | ------------------------------------- | --------------------------------- |
7
7
  | `SUPABASE_URL` | `https://<ref>.supabase.co` | Your Supabase project URL | All |
8
- | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_..."}` | Named publishable keys as JSON object | Platform, Local Development (CLI) |
9
- | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_..."}` | Named secret keys as JSON object | Platform, Local Development (CLI) |
10
- | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | JSON Web Key Set for JWT verification | Platform, Local Development (CLI) |
11
- | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key (fallback) | Self-hosted |
12
- | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key (fallback) | Self-hosted |
8
+ | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_..."}` | Named publishable keys as JSON object | All |
9
+ | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_..."}` | Named secret keys as JSON object | All |
10
+ | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | JSON Web Key Set for JWT verification | All |
11
+ | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key (fallback) | Self-hosted, if manually exported |
12
+ | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key (fallback) | Self-hosted, if manually exported |
13
13
 
14
14
  ## Non-Supabase environments (Node.js, Bun, Cloudflare, self-hosted)
15
15
 
16
16
  Set these based on which auth modes your app uses:
17
17
 
18
- | Variable | Required when |
19
- | -------------------------- | ------------------------------------------ |
20
- | `SUPABASE_URL` | Always |
21
- | `SUPABASE_SECRET_KEY` | `allow: 'secret'` or using `supabaseAdmin` |
22
- | `SUPABASE_PUBLISHABLE_KEY` | `allow: 'public'` |
23
- | `SUPABASE_JWKS` | `allow: 'user'` (JWT verification) |
18
+ | Variable | Required when |
19
+ | -------------------------- | ----------------------------------------- |
20
+ | `SUPABASE_URL` | Always |
21
+ | `SUPABASE_SECRET_KEY` | `auth: 'secret'` or using `supabaseAdmin` |
22
+ | `SUPABASE_PUBLISHABLE_KEY` | `auth: 'publishable'` |
23
+ | `SUPABASE_JWKS` | `auth: 'user'` (JWT verification) |
24
24
 
25
25
  ### Minimal `.env` example
26
26
 
@@ -48,10 +48,10 @@ You can then validate against specific keys with named key syntax:
48
48
 
49
49
  ```ts
50
50
  // Only accept the "web" publishable key
51
- withSupabase({ allow: 'public:web' }, handler)
51
+ withSupabase({ auth: 'publishable:web' }, handler)
52
52
 
53
53
  // Accept any secret key
54
- withSupabase({ allow: 'secret:*' }, handler)
54
+ withSupabase({ auth: 'secret:*' }, handler)
55
55
  ```
56
56
 
57
57
  ### Singular form — equivalent to a single "default" key
@@ -69,7 +69,7 @@ SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
69
69
  SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc"}
70
70
  ```
71
71
 
72
- The singular form is a convenience for the common case where you only have one key. The SDK stores it internally as `{ default: "<value>" }`, so `allow: 'public'` (which looks for the `"default"` key) works with both forms.
72
+ The singular form is a convenience for the common case where you only have one key. The SDK stores it internally as `{ default: "<value>" }`, so `auth: 'publishable'` (which looks for the `"default"` key) works with both forms.
73
73
 
74
74
  ### Priority
75
75
 
@@ -87,7 +87,7 @@ SUPABASE_JWKS={"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]}
87
87
  SUPABASE_JWKS=[{"kty":"RSA","n":"...","e":"AQAB"}]
88
88
  ```
89
89
 
90
- When `SUPABASE_JWKS` is not set, JWT verification (`allow: 'user'`) is unavailable.
90
+ When `SUPABASE_JWKS` is not set, JWT verification (`auth: 'user'`) is unavailable.
91
91
 
92
92
  ## Runtime-specific behavior
93
93
 
@@ -119,7 +119,7 @@ Cloudflare Workers don't expose `Deno.env` or `process.env` by default. Two opti
119
119
  ```ts
120
120
  withSupabase(
121
121
  {
122
- allow: 'user',
122
+ auth: 'user',
123
123
  env: {
124
124
  url: env.SUPABASE_URL,
125
125
  publishableKeys: { default: env.SUPABASE_PUBLISHABLE_KEY },
@@ -140,7 +140,7 @@ import { withSupabase } from '@supabase/server'
140
140
  export default {
141
141
  fetch: withSupabase(
142
142
  {
143
- allow: 'user',
143
+ auth: 'user',
144
144
  env: {
145
145
  url: 'http://localhost:54321', // override just the URL
146
146
  },
@@ -62,7 +62,7 @@ import { createSupabaseContext } from '@supabase/server'
62
62
  export default {
63
63
  fetch: async (req: Request) => {
64
64
  const { data: ctx, error } = await createSupabaseContext(req, {
65
- allow: 'user',
65
+ auth: 'user',
66
66
  })
67
67
 
68
68
  if (error) {
@@ -93,7 +93,7 @@ import { withSupabase } from '@supabase/server/adapters/hono'
93
93
 
94
94
  const app = new Hono()
95
95
 
96
- app.use('*', withSupabase({ allow: 'user' }))
96
+ app.use('*', withSupabase({ auth: 'user' }))
97
97
 
98
98
  app.onError((err, c) => {
99
99
  if (err instanceof HTTPException && err.cause) {
@@ -115,7 +115,7 @@ Result-tuple functions:
115
115
  import { verifyAuth, resolveEnv } from '@supabase/server/core'
116
116
 
117
117
  // verifyAuth returns { data, error }
118
- const { data: auth, error } = await verifyAuth(request, { allow: 'user' })
118
+ const { data: auth, error } = await verifyAuth(request, { auth: 'user' })
119
119
  if (error) {
120
120
  return Response.json({ message: error.message }, { status: error.status })
121
121
  }
@@ -137,7 +137,7 @@ import {
137
137
  } from '@supabase/server/core'
138
138
  import { EnvError } from '@supabase/server'
139
139
 
140
- const { data: auth, error } = await verifyAuth(request, { allow: 'user' })
140
+ const { data: auth, error } = await verifyAuth(request, { auth: 'user' })
141
141
  // ... handle error ...
142
142
 
143
143
  try {