@supabase/server 0.1.1 → 0.1.2
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 +73 -6
- package/SKILL.md +415 -0
- package/dist/adapters/hono/index.cjs +1 -1
- package/dist/adapters/hono/index.d.cts +1 -1
- package/dist/adapters/hono/index.d.mts +1 -1
- package/dist/adapters/hono/index.mjs +1 -1
- package/dist/core/index.d.cts +1 -1
- package/dist/core/index.d.mts +1 -1
- package/dist/{create-supabase-context-DDIAxA8h.cjs → create-supabase-context--VqMJpDu.cjs} +4 -2
- package/dist/{create-supabase-context-Bmwyha9p.mjs → create-supabase-context-B3Uzt_3I.mjs} +4 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{types-BmWSIuH7.d.mts → types-CbC-wBUe.d.mts} +4 -2
- package/dist/{types-X7xYi2LN.d.cts → types-DxTr0Qum.d.cts} +4 -2
- package/docs/api-reference.md +322 -0
- package/docs/auth-modes.md +203 -0
- package/docs/core-primitives.md +240 -0
- package/docs/environment-variables.md +189 -0
- package/docs/error-handling.md +191 -0
- package/docs/getting-started.md +149 -0
- package/docs/hono-adapter.md +185 -0
- package/docs/security.md +82 -0
- package/docs/ssr-frameworks.md +330 -0
- package/docs/typescript-generics.md +143 -0
- package/package.json +4 -7
- package/dist/wrappers/index.cjs +0 -45
- package/dist/wrappers/index.d.cts +0 -23
- package/dist/wrappers/index.d.mts +0 -23
- package/dist/wrappers/index.mjs +0 -43
|
@@ -52,12 +52,12 @@ interface SupabaseEnv {
|
|
|
52
52
|
/** Supabase project URL (e.g. `"https://<ref>.supabase.co"`). Sourced from `SUPABASE_URL`. */
|
|
53
53
|
url: string;
|
|
54
54
|
/**
|
|
55
|
-
* Named publishable
|
|
55
|
+
* Named publishable keys. Sourced from `SUPABASE_PUBLISHABLE_KEYS` (JSON object)
|
|
56
56
|
* or `SUPABASE_PUBLISHABLE_KEY` (single key, stored as `{ default: "<value>" }`).
|
|
57
57
|
*/
|
|
58
58
|
publishableKeys: Record<string, string>;
|
|
59
59
|
/**
|
|
60
|
-
* Named secret
|
|
60
|
+
* Named secret keys. Sourced from `SUPABASE_SECRET_KEYS` (JSON object)
|
|
61
61
|
* or `SUPABASE_SECRET_KEY` (single key, stored as `{ default: "<value>" }`).
|
|
62
62
|
*/
|
|
63
63
|
secretKeys: Record<string, string>;
|
|
@@ -266,6 +266,8 @@ interface SupabaseContext<Database = unknown> {
|
|
|
266
266
|
claims: JWTClaims | null;
|
|
267
267
|
/** The auth mode that was used for this request. */
|
|
268
268
|
authType: Allow;
|
|
269
|
+
/** The auth key name of the API key that was used for this request. */
|
|
270
|
+
authKeyName?: string | null;
|
|
269
271
|
}
|
|
270
272
|
//#endregion
|
|
271
273
|
export { CreateAdminClientOptions as a, JWTClaims as c, UserClaims as d, WithSupabaseConfig as f, ClientAuth as i, SupabaseContext as l, AllowWithKey as n, CreateContextClientOptions as o, AuthResult as r, Credentials as s, Allow as t, SupabaseEnv as u };
|
|
@@ -52,12 +52,12 @@ interface SupabaseEnv {
|
|
|
52
52
|
/** Supabase project URL (e.g. `"https://<ref>.supabase.co"`). Sourced from `SUPABASE_URL`. */
|
|
53
53
|
url: string;
|
|
54
54
|
/**
|
|
55
|
-
* Named publishable
|
|
55
|
+
* Named publishable keys. Sourced from `SUPABASE_PUBLISHABLE_KEYS` (JSON object)
|
|
56
56
|
* or `SUPABASE_PUBLISHABLE_KEY` (single key, stored as `{ default: "<value>" }`).
|
|
57
57
|
*/
|
|
58
58
|
publishableKeys: Record<string, string>;
|
|
59
59
|
/**
|
|
60
|
-
* Named secret
|
|
60
|
+
* Named secret keys. Sourced from `SUPABASE_SECRET_KEYS` (JSON object)
|
|
61
61
|
* or `SUPABASE_SECRET_KEY` (single key, stored as `{ default: "<value>" }`).
|
|
62
62
|
*/
|
|
63
63
|
secretKeys: Record<string, string>;
|
|
@@ -266,6 +266,8 @@ interface SupabaseContext<Database = unknown> {
|
|
|
266
266
|
claims: JWTClaims | null;
|
|
267
267
|
/** The auth mode that was used for this request. */
|
|
268
268
|
authType: Allow;
|
|
269
|
+
/** The auth key name of the API key that was used for this request. */
|
|
270
|
+
authKeyName?: string | null;
|
|
269
271
|
}
|
|
270
272
|
//#endregion
|
|
271
273
|
export { CreateAdminClientOptions as a, JWTClaims as c, UserClaims as d, WithSupabaseConfig as f, ClientAuth as i, SupabaseContext as l, AllowWithKey as n, CreateContextClientOptions as o, AuthResult as r, Credentials as s, Allow as t, SupabaseEnv as u };
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for every export, organized by entry point.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## @supabase/server
|
|
8
|
+
|
|
9
|
+
### withSupabase
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
function withSupabase<Database = unknown>(
|
|
13
|
+
config: WithSupabaseConfig,
|
|
14
|
+
handler: (req: Request, ctx: SupabaseContext<Database>) => Promise<Response>,
|
|
15
|
+
): (req: Request) => Promise<Response>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Wraps a fetch handler with auth, CORS, and client creation. Returns a `(req: Request) => Promise<Response>` function suitable for `export default { fetch }`.
|
|
19
|
+
|
|
20
|
+
- Handles `OPTIONS` preflight when CORS is enabled
|
|
21
|
+
- Verifies credentials per `config.allow`
|
|
22
|
+
- Returns JSON error response on auth failure
|
|
23
|
+
- Adds CORS headers to all responses
|
|
24
|
+
|
|
25
|
+
### createSupabaseContext
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
function createSupabaseContext<Database = unknown>(
|
|
29
|
+
request: Request,
|
|
30
|
+
options?: WithSupabaseConfig,
|
|
31
|
+
): Promise<
|
|
32
|
+
| { data: SupabaseContext<Database>; error: null }
|
|
33
|
+
| { data: null; error: AuthError }
|
|
34
|
+
>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Creates a `SupabaseContext` from a request. Returns a result tuple. The `cors` option is ignored.
|
|
38
|
+
|
|
39
|
+
Defaults to `allow: 'user'` when `options` is omitted.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## @supabase/server/core
|
|
44
|
+
|
|
45
|
+
### verifyAuth
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
function verifyAuth(
|
|
49
|
+
request: Request,
|
|
50
|
+
options: { allow: AllowWithKey | AllowWithKey[]; env?: Partial<SupabaseEnv> },
|
|
51
|
+
): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Extracts credentials from a request and verifies them. Convenience wrapper over `extractCredentials` + `verifyCredentials`.
|
|
55
|
+
|
|
56
|
+
### verifyCredentials
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
function verifyCredentials(
|
|
60
|
+
credentials: Credentials,
|
|
61
|
+
options: { allow: AllowWithKey | AllowWithKey[]; env?: Partial<SupabaseEnv> },
|
|
62
|
+
): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Verifies pre-extracted credentials against allowed auth modes. Tries each mode in order — first match wins.
|
|
66
|
+
|
|
67
|
+
### extractCredentials
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
function extractCredentials(request: Request): Credentials
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Reads `Authorization: Bearer <token>` and `apikey` headers from a request. Pure extraction, no validation. Synchronous.
|
|
74
|
+
|
|
75
|
+
### resolveEnv
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
function resolveEnv(
|
|
79
|
+
overrides?: Partial<SupabaseEnv>,
|
|
80
|
+
): { data: SupabaseEnv; error: null } | { data: null; error: EnvError }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Resolves Supabase environment configuration from runtime variables. `SUPABASE_URL` is the only hard requirement.
|
|
84
|
+
|
|
85
|
+
### createContextClient
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
function createContextClient<Database = unknown>(
|
|
89
|
+
options?: CreateContextClientOptions,
|
|
90
|
+
): SupabaseClient<Database>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Creates a user-scoped Supabase client. RLS applies. **Throws `EnvError`** if URL or publishable key is missing.
|
|
94
|
+
|
|
95
|
+
Configured with:
|
|
96
|
+
|
|
97
|
+
- Publishable key (named or default) as `apikey` header
|
|
98
|
+
- User's JWT as `Authorization: Bearer` header (when `auth.token` is provided)
|
|
99
|
+
- `persistSession: false`, `autoRefreshToken: false`, `detectSessionInUrl: false`
|
|
100
|
+
|
|
101
|
+
### createAdminClient
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
function createAdminClient<Database = unknown>(
|
|
105
|
+
options?: CreateAdminClientOptions,
|
|
106
|
+
): SupabaseClient<Database>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Creates an admin Supabase client that bypasses RLS. **Throws `EnvError`** if URL or secret key is missing.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## @supabase/server/adapters/hono
|
|
114
|
+
|
|
115
|
+
### withSupabase (Hono)
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
function withSupabase(
|
|
119
|
+
config?: Omit<WithSupabaseConfig, 'cors'>,
|
|
120
|
+
): MiddlewareHandler
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Hono middleware. Sets `c.var.supabaseContext` on the Hono context. Throws `HTTPException` on auth failure with `cause: AuthError`.
|
|
124
|
+
|
|
125
|
+
Skips if `c.var.supabaseContext` is already set (enables route-level overrides).
|
|
126
|
+
|
|
127
|
+
Defaults to `allow: 'user'` when config is omitted.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Types
|
|
132
|
+
|
|
133
|
+
### Allow
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
type Allow = 'always' | 'public' | 'secret' | 'user'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### AllowWithKey
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
type AllowWithKey = Allow | `public:${string}` | `secret:${string}`
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Extended auth mode with named key support. Examples: `'public:web'`, `'secret:*'`, `'secret:internal'`.
|
|
146
|
+
|
|
147
|
+
### SupabaseContext\<Database\>
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
interface SupabaseContext<Database = unknown> {
|
|
151
|
+
supabase: SupabaseClient<Database>
|
|
152
|
+
supabaseAdmin: SupabaseClient<Database>
|
|
153
|
+
userClaims: UserClaims | null
|
|
154
|
+
claims: JWTClaims | null
|
|
155
|
+
authType: Allow
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### WithSupabaseConfig
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
interface WithSupabaseConfig {
|
|
163
|
+
allow?: AllowWithKey | AllowWithKey[] // default: 'user'
|
|
164
|
+
env?: Partial<SupabaseEnv>
|
|
165
|
+
cors?: boolean | Record<string, string> // default: true
|
|
166
|
+
supabaseOptions?: SupabaseClientOptions<string>
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### SupabaseEnv
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
interface SupabaseEnv {
|
|
174
|
+
url: string
|
|
175
|
+
publishableKeys: Record<string, string>
|
|
176
|
+
secretKeys: Record<string, string>
|
|
177
|
+
jwks: JsonWebKeySet | null
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Credentials
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
interface Credentials {
|
|
185
|
+
token: string | null
|
|
186
|
+
apikey: string | null
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### AuthResult
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
interface AuthResult {
|
|
194
|
+
authType: Allow
|
|
195
|
+
token: string | null
|
|
196
|
+
userClaims: UserClaims | null
|
|
197
|
+
claims: JWTClaims | null
|
|
198
|
+
keyName?: string | null
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### JWTClaims
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
interface JWTClaims {
|
|
206
|
+
sub: string
|
|
207
|
+
iss?: string
|
|
208
|
+
aud?: string | string[]
|
|
209
|
+
exp?: number
|
|
210
|
+
iat?: number
|
|
211
|
+
role?: string
|
|
212
|
+
email?: string
|
|
213
|
+
app_metadata?: Record<string, unknown>
|
|
214
|
+
user_metadata?: Record<string, unknown>
|
|
215
|
+
[key: string]: unknown
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### UserClaims
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
interface UserClaims {
|
|
223
|
+
id: string
|
|
224
|
+
role?: string
|
|
225
|
+
email?: string
|
|
226
|
+
appMetadata?: Record<string, unknown>
|
|
227
|
+
userMetadata?: Record<string, unknown>
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### ClientAuth
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
interface ClientAuth {
|
|
235
|
+
token?: string | null
|
|
236
|
+
keyName?: string | null
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### CreateContextClientOptions
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
interface CreateContextClientOptions {
|
|
244
|
+
auth?: ClientAuth
|
|
245
|
+
env?: Partial<SupabaseEnv>
|
|
246
|
+
supabaseOptions?: SupabaseClientOptions<string>
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### CreateAdminClientOptions
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
interface CreateAdminClientOptions {
|
|
254
|
+
auth?: Pick<ClientAuth, 'keyName'>
|
|
255
|
+
env?: Partial<SupabaseEnv>
|
|
256
|
+
supabaseOptions?: SupabaseClientOptions<string>
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### JsonWebKeySet
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
interface JsonWebKeySet {
|
|
264
|
+
keys: JsonWebKey[]
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Error Classes
|
|
271
|
+
|
|
272
|
+
### EnvError
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
class EnvError extends Error {
|
|
276
|
+
readonly status: 500
|
|
277
|
+
readonly code: string
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### AuthError
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
class AuthError extends Error {
|
|
285
|
+
readonly status: number // 401 or 500
|
|
286
|
+
readonly code: string
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Error Code Constants
|
|
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 |
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Errors Factory Map
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const Errors: {
|
|
312
|
+
[MissingSupabaseURLError]: () => EnvError
|
|
313
|
+
[MissingPublishableKeyError]: (name: string) => EnvError
|
|
314
|
+
[MissingDefaultPublishableKeyError]: () => EnvError
|
|
315
|
+
[MissingSecretKeyError]: (name: string) => EnvError
|
|
316
|
+
[MissingDefaultSecretKeyError]: () => EnvError
|
|
317
|
+
[InvalidCredentialsError]: () => AuthError
|
|
318
|
+
[CreateSupabaseClientError]: () => AuthError
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Keyed by error code constant. Each entry returns a pre-configured error instance.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Auth Modes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Every request is validated against one or more auth modes before your handler runs. The `allow` config determines which modes are accepted.
|
|
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 |
|
|
13
|
+
|
|
14
|
+
> **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`:
|
|
16
|
+
>
|
|
17
|
+
> ```toml
|
|
18
|
+
> [functions.my-function]
|
|
19
|
+
> verify_jwt = false
|
|
20
|
+
> ```
|
|
21
|
+
|
|
22
|
+
## User mode
|
|
23
|
+
|
|
24
|
+
The default. Verifies the JWT using your project's JWKS (JSON Web Key Set).
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { withSupabase } from '@supabase/server'
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
|
|
31
|
+
// ctx.userClaims has the caller's identity
|
|
32
|
+
console.log(ctx.userClaims!.id) // "d0f1a2b3-..."
|
|
33
|
+
console.log(ctx.userClaims!.email) // "user@example.com"
|
|
34
|
+
console.log(ctx.userClaims!.role) // "authenticated"
|
|
35
|
+
|
|
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)
|
|
39
|
+
|
|
40
|
+
// ctx.supabase is scoped to this user — RLS applies
|
|
41
|
+
const { data } = await ctx.supabase.from('todos').select()
|
|
42
|
+
return Response.json(data)
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The caller must send:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**`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
|
+
|
|
55
|
+
## Public mode
|
|
56
|
+
|
|
57
|
+
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
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { withSupabase } from '@supabase/server'
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
fetch: withSupabase({ allow: 'public' }, async (_req, ctx) => {
|
|
64
|
+
// ctx.userClaims is null — no JWT involved
|
|
65
|
+
// ctx.supabase is initialized as anonymous (RLS anon role)
|
|
66
|
+
const { data } = await ctx.supabase.from('products').select()
|
|
67
|
+
return Response.json(data)
|
|
68
|
+
}),
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The caller must send:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
apikey: sb_publishable_abc123...
|
|
76
|
+
```
|
|
77
|
+
|
|
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).
|
|
79
|
+
|
|
80
|
+
## Secret mode
|
|
81
|
+
|
|
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.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { withSupabase } from '@supabase/server'
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
|
|
89
|
+
// ctx.supabaseAdmin bypasses RLS — use for privileged operations
|
|
90
|
+
const { data } = await ctx.supabaseAdmin.from('config').select()
|
|
91
|
+
return Response.json(data)
|
|
92
|
+
}),
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The caller must send:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
apikey: sb_secret_xyz789...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Always mode
|
|
103
|
+
|
|
104
|
+
No credentials required. Every request is accepted.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { withSupabase } from '@supabase/server'
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
fetch: withSupabase({ allow: 'always' }, async (_req, ctx) => {
|
|
111
|
+
// ctx.authType is 'always'
|
|
112
|
+
// ctx.userClaims is null
|
|
113
|
+
// ctx.supabase is anonymous (RLS anon role)
|
|
114
|
+
return Response.json({ status: 'healthy' })
|
|
115
|
+
}),
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use `always` for health checks, public APIs, or when you handle auth yourself inside the handler.
|
|
120
|
+
|
|
121
|
+
## Array syntax (multiple modes)
|
|
122
|
+
|
|
123
|
+
Accept multiple auth methods. Modes are tried in order — the first match wins.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { withSupabase } from '@supabase/server'
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
|
|
130
|
+
// ctx.authType tells you which mode matched
|
|
131
|
+
if (ctx.authType === 'user') {
|
|
132
|
+
// Called by an authenticated user
|
|
133
|
+
const { data } = await ctx.supabase.from('reports').select()
|
|
134
|
+
return Response.json(data)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Called by another service with a secret key
|
|
138
|
+
const { user_id } = await req.json()
|
|
139
|
+
const { data } = await ctx.supabaseAdmin
|
|
140
|
+
.from('reports')
|
|
141
|
+
.select()
|
|
142
|
+
.eq('user_id', user_id)
|
|
143
|
+
return Response.json(data)
|
|
144
|
+
}),
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
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
|
+
|
|
150
|
+
## Named key syntax
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
Keys are stored as a JSON object in `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"default": "sb_publishable_123...",
|
|
159
|
+
"web": "sb_publishable_abc...",
|
|
160
|
+
"mobile": "sb_publishable_a1b2..."
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Target a specific key
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// Only accept the "web" publishable key
|
|
168
|
+
withSupabase({ allow: 'public:web' }, handler)
|
|
169
|
+
|
|
170
|
+
// Only accept the "internal" secret key
|
|
171
|
+
withSupabase({ allow: 'secret:internal' }, handler)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Wildcard — accept any key in the set
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// Accept any publishable key
|
|
178
|
+
withSupabase({ allow: 'public:*' }, handler)
|
|
179
|
+
|
|
180
|
+
// Accept any secret key
|
|
181
|
+
withSupabase({ allow: 'secret:*' }, handler)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Which key matched?
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
### Combining named keys with other modes
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
withSupabase({ allow: ['user', 'public:web'] }, async (_req, ctx) => {
|
|
192
|
+
// Accepts either a valid JWT or the "web" publishable key
|
|
193
|
+
return Response.json({ authType: ctx.authType })
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## How auth flows through the system
|
|
198
|
+
|
|
199
|
+
1. `extractCredentials(request)` reads `Authorization: Bearer <token>` and `apikey` from headers
|
|
200
|
+
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`
|
|
202
|
+
4. The auth result is used to create scoped clients (`supabase` with the user's token, `supabaseAdmin` with the secret key)
|
|
203
|
+
5. Everything is bundled into a `SupabaseContext` and passed to your handler
|