@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.
- package/README.md +93 -92
- package/dist/adapters/h3/index.cjs +3 -3
- package/dist/adapters/h3/index.d.cts +3 -3
- package/dist/adapters/h3/index.d.mts +3 -3
- package/dist/adapters/h3/index.mjs +3 -3
- package/dist/adapters/hono/index.cjs +2 -2
- package/dist/adapters/hono/index.d.cts +2 -2
- package/dist/adapters/hono/index.d.mts +2 -2
- package/dist/adapters/hono/index.mjs +2 -2
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.d.cts +25 -9
- package/dist/core/index.d.mts +25 -9
- package/dist/core/index.mjs +1 -1
- package/dist/{create-supabase-context-C_8SbO5w.cjs → create-supabase-context-B-2NDJhL.cjs} +10 -9
- package/dist/{create-supabase-context-DXD5rxi1.mjs → create-supabase-context-BBZtr3D2.mjs} +10 -9
- package/dist/{errors-Dyj5Cjt6.d.cts → errors-0dbzn5gA.d.mts} +1 -1
- package/dist/{errors-m42mkqhD.d.mts → errors-CZFEYnV_.d.cts} +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +3 -3
- package/dist/{types-DKe8uOwI.d.mts → types-B2yXZjmG.d.mts} +40 -23
- package/dist/{types-DqhOaSlC.d.cts → types-u7fYLtzC.d.cts} +40 -23
- package/dist/{verify-auth-C4zqDlfj.cjs → verify-auth-BKZK83Y8.cjs} +66 -34
- package/dist/{verify-auth-CxFZy9rl.mjs → verify-auth-CZQd36s0.mjs} +66 -34
- package/docs/adapters/h3.md +180 -0
- package/docs/{hono-adapter.md → adapters/hono.md} +14 -25
- package/docs/api-reference.md +28 -15
- package/docs/auth-modes.md +38 -34
- package/docs/core-primitives.md +13 -13
- package/docs/environment-variables.md +17 -17
- package/docs/error-handling.md +4 -4
- package/docs/getting-started.md +17 -17
- package/docs/security.md +15 -15
- package/docs/ssr-frameworks.md +148 -172
- package/docs/typescript-generics.md +6 -6
- package/package.json +5 -3
- 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({
|
|
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`, `
|
|
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({
|
|
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({
|
|
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({
|
|
75
|
-
const { supabase,
|
|
76
|
-
return c.json({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
auth: 'user',
|
|
182
171
|
supabaseOptions: { db: { schema: 'api' } },
|
|
183
172
|
}),
|
|
184
173
|
)
|
package/docs/api-reference.md
CHANGED
|
@@ -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.
|
|
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 `
|
|
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: {
|
|
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: {
|
|
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 `
|
|
133
|
+
Defaults to `auth: 'user'` when config is omitted.
|
|
128
134
|
|
|
129
135
|
---
|
|
130
136
|
|
|
131
137
|
## Types
|
|
132
138
|
|
|
133
|
-
###
|
|
139
|
+
### AuthMode
|
|
134
140
|
|
|
135
141
|
```ts
|
|
136
|
-
type
|
|
142
|
+
type AuthMode = 'none' | 'publishable' | 'secret' | 'user'
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
###
|
|
145
|
+
### AuthModeWithKey
|
|
140
146
|
|
|
141
147
|
```ts
|
|
142
|
-
type
|
|
148
|
+
type AuthModeWithKey = AuthMode | `publishable:${string}` | `secret:${string}`
|
|
143
149
|
```
|
|
144
150
|
|
|
145
|
-
Extended auth mode with named key support. Examples: `'
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
authMode: AuthMode
|
|
195
208
|
token: string | null
|
|
196
209
|
userClaims: UserClaims | null
|
|
197
|
-
|
|
210
|
+
jwtClaims: JWTClaims | null
|
|
198
211
|
keyName?: string | null
|
|
199
212
|
}
|
|
200
213
|
```
|
package/docs/auth-modes.md
CHANGED
|
@@ -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 `
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
|
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 `'
|
|
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({
|
|
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.
|
|
37
|
-
console.log(ctx.
|
|
38
|
-
console.log(ctx.
|
|
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
|
-
##
|
|
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({
|
|
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, `
|
|
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
|
|
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({
|
|
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
|
-
##
|
|
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({
|
|
111
|
-
// ctx.
|
|
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 `
|
|
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({
|
|
130
|
-
// ctx.
|
|
131
|
-
if (ctx.
|
|
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 `'
|
|
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({
|
|
174
|
+
withSupabase({ auth: 'publishable:web' }, handler)
|
|
171
175
|
|
|
172
176
|
// Only accept the "internal" secret key
|
|
173
|
-
withSupabase({
|
|
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({
|
|
184
|
+
withSupabase({ auth: 'publishable:*' }, handler)
|
|
181
185
|
|
|
182
186
|
// Accept any secret key
|
|
183
|
-
withSupabase({
|
|
187
|
+
withSupabase({ auth: 'secret:*' }, handler)
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
### Which key matched?
|
|
187
191
|
|
|
188
|
-
When using named keys, `ctx.
|
|
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({
|
|
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({
|
|
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 `
|
|
203
|
-
3. First match wins — returns an `AuthResult` with `
|
|
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
|
package/docs/core-primitives.md
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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!.
|
|
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
|
-
|
|
96
|
+
auth: ['user', 'publishable'],
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
// Named key
|
|
100
100
|
const { data: auth } = await verifyCredentials(creds, {
|
|
101
|
-
|
|
101
|
+
auth: 'publishable:web',
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
// Wildcard
|
|
105
105
|
const { data: auth } = await verifyCredentials(creds, {
|
|
106
|
-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
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
|
-
##
|
|
236
|
+
## Cookie-based environments (with `@supabase/ssr`)
|
|
237
237
|
|
|
238
|
-
In
|
|
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
|
|
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 |
|
|
9
|
-
| `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_..."}` | Named secret keys as JSON object |
|
|
10
|
-
| `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | JSON Web Key Set for JWT verification |
|
|
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` | `
|
|
22
|
-
| `SUPABASE_PUBLISHABLE_KEY` | `
|
|
23
|
-
| `SUPABASE_JWKS` | `
|
|
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({
|
|
51
|
+
withSupabase({ auth: 'publishable:web' }, handler)
|
|
52
52
|
|
|
53
53
|
// Accept any secret key
|
|
54
|
-
withSupabase({
|
|
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 `
|
|
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 (`
|
|
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
|
-
|
|
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
|
-
|
|
143
|
+
auth: 'user',
|
|
144
144
|
env: {
|
|
145
145
|
url: 'http://localhost:54321', // override just the URL
|
|
146
146
|
},
|
package/docs/error-handling.md
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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, {
|
|
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, {
|
|
140
|
+
const { data: auth, error } = await verifyAuth(request, { auth: 'user' })
|
|
141
141
|
// ... handle error ...
|
|
142
142
|
|
|
143
143
|
try {
|