@supabase/server 0.1.1-rc.28 → 0.1.2-rc.32
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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Core Primitives
|
|
2
|
+
|
|
3
|
+
## When to use primitives
|
|
4
|
+
|
|
5
|
+
Use `withSupabase` or `createSupabaseContext` for standard use cases. Drop down to core primitives when you need:
|
|
6
|
+
|
|
7
|
+
- Multiple routes with different auth in a single handler
|
|
8
|
+
- Custom response headers or error formats
|
|
9
|
+
- Integration with frameworks other than the ones provided
|
|
10
|
+
- Pre-extracted credentials (e.g., from cookies, custom headers)
|
|
11
|
+
- Just auth verification without client creation
|
|
12
|
+
|
|
13
|
+
All primitives are available from `@supabase/server/core`.
|
|
14
|
+
|
|
15
|
+
## The composition pipeline
|
|
16
|
+
|
|
17
|
+
The primitives compose into a pipeline. Each step is independent — use only what you need:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
resolveEnv() → SupabaseEnv
|
|
21
|
+
extractCredentials(request) → Credentials { token, apikey }
|
|
22
|
+
verifyCredentials(credentials, opts) → AuthResult { authType, token, userClaims, claims, keyName }
|
|
23
|
+
createContextClient(options) → SupabaseClient (RLS-scoped)
|
|
24
|
+
createAdminClient(options) → SupabaseClient (bypasses RLS)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or use the convenience function that combines extraction and verification:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
verifyAuth(request, opts) → AuthResult (extractCredentials + verifyCredentials in one call)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## resolveEnv
|
|
34
|
+
|
|
35
|
+
Resolves Supabase environment configuration from runtime variables. The only hard requirement is `SUPABASE_URL`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { resolveEnv } from '@supabase/server/core'
|
|
39
|
+
|
|
40
|
+
const { data: env, error } = resolveEnv()
|
|
41
|
+
if (error) {
|
|
42
|
+
// error is an EnvError — e.g., SUPABASE_URL not set
|
|
43
|
+
console.error(error.message)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With partial overrides:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const { data: envOverridden } = resolveEnv({
|
|
51
|
+
url: 'http://localhost:54321',
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
|
|
56
|
+
|
|
57
|
+
## extractCredentials
|
|
58
|
+
|
|
59
|
+
Pure extraction — reads headers, performs no validation.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { extractCredentials } from '@supabase/server/core'
|
|
63
|
+
|
|
64
|
+
const creds = extractCredentials(request)
|
|
65
|
+
// creds.token → string | null (from Authorization: Bearer <token>)
|
|
66
|
+
// creds.apikey → string | null (from apikey header)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This is synchronous and never fails. Fields are `null` when the corresponding header is absent.
|
|
70
|
+
|
|
71
|
+
## verifyCredentials
|
|
72
|
+
|
|
73
|
+
Verifies pre-extracted credentials against allowed auth modes. Use this when credentials come from a non-standard source (cookies, custom headers, etc.).
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { verifyCredentials } from '@supabase/server/core'
|
|
77
|
+
|
|
78
|
+
const credentials = { token: cookieToken, apikey: null }
|
|
79
|
+
const { data: auth, error } = await verifyCredentials(credentials, {
|
|
80
|
+
allow: 'user',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
return Response.json({ message: error.message }, { status: error.status })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(auth!.authType) // 'user'
|
|
88
|
+
console.log(auth!.userClaims) // { id: '...', email: '...', role: 'authenticated' }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supports all auth mode syntax — single mode, arrays, and named keys:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// Multiple modes
|
|
95
|
+
const { data: auth } = await verifyCredentials(creds, {
|
|
96
|
+
allow: ['user', 'public'],
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Named key
|
|
100
|
+
const { data: auth } = await verifyCredentials(creds, {
|
|
101
|
+
allow: 'public:web',
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Wildcard
|
|
105
|
+
const { data: auth } = await verifyCredentials(creds, {
|
|
106
|
+
allow: 'secret:*',
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## verifyAuth
|
|
111
|
+
|
|
112
|
+
Convenience function that combines `extractCredentials` and `verifyCredentials` in a single call. Use this when working with a standard `Request`:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { verifyAuth } from '@supabase/server/core'
|
|
116
|
+
|
|
117
|
+
const { data: auth, error } = await verifyAuth(request, {
|
|
118
|
+
allow: 'user',
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (error) {
|
|
122
|
+
return Response.json({ message: error.message }, { status: error.status })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(auth.userClaims!.id) // "d0f1a2b3-..."
|
|
126
|
+
console.log(auth.token) // the verified JWT string
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## createContextClient
|
|
130
|
+
|
|
131
|
+
Creates a Supabase client scoped to the caller's identity. RLS policies apply.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { verifyAuth, createContextClient } from '@supabase/server/core'
|
|
135
|
+
|
|
136
|
+
// With a user's token (from verifyAuth)
|
|
137
|
+
const { data: auth } = await verifyAuth(request, { allow: 'user' })
|
|
138
|
+
const supabase = createContextClient({
|
|
139
|
+
auth: { token: auth!.token, keyName: auth!.keyName },
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Anonymous (no token) — RLS as anon role
|
|
145
|
+
const anonClient = createContextClient()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The client is configured with:
|
|
149
|
+
|
|
150
|
+
- The publishable key as the `apikey` header
|
|
151
|
+
- The user's JWT as the `Authorization: Bearer` header (if token is provided)
|
|
152
|
+
- Server-safe auth settings: `persistSession: false`, `autoRefreshToken: false`, `detectSessionInUrl: false`
|
|
153
|
+
|
|
154
|
+
This function throws `EnvError` if `SUPABASE_URL` or the required publishable key is missing. Wrap in try/catch when using directly.
|
|
155
|
+
|
|
156
|
+
## createAdminClient
|
|
157
|
+
|
|
158
|
+
Creates a Supabase client that bypasses Row-Level Security using a secret key.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { createAdminClient } from '@supabase/server/core'
|
|
162
|
+
|
|
163
|
+
const supabaseAdmin = createAdminClient()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// With a specific named key
|
|
168
|
+
const supabaseAdminInternal = createAdminClient({
|
|
169
|
+
auth: { keyName: 'internal' },
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Same server-safe settings as `createContextClient`. Throws `EnvError` if the secret key is missing.
|
|
174
|
+
|
|
175
|
+
## Full example: custom multi-route handler
|
|
176
|
+
|
|
177
|
+
Using primitives to build a handler with different auth per route, without a framework:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import {
|
|
181
|
+
verifyAuth,
|
|
182
|
+
createContextClient,
|
|
183
|
+
createAdminClient,
|
|
184
|
+
} from '@supabase/server/core'
|
|
185
|
+
|
|
186
|
+
export default {
|
|
187
|
+
fetch: async (req: Request) => {
|
|
188
|
+
const url = new URL(req.url)
|
|
189
|
+
|
|
190
|
+
// Public route — no auth needed
|
|
191
|
+
if (url.pathname === '/health') {
|
|
192
|
+
return Response.json({ status: 'ok' })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// User-authenticated route
|
|
196
|
+
if (url.pathname === '/todos') {
|
|
197
|
+
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
|
|
198
|
+
if (error) {
|
|
199
|
+
return Response.json(
|
|
200
|
+
{ message: error.message },
|
|
201
|
+
{ status: error.status },
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const supabase = createContextClient({
|
|
206
|
+
auth: { token: auth!.token, keyName: auth!.keyName },
|
|
207
|
+
})
|
|
208
|
+
const { data } = await supabase.from('todos').select()
|
|
209
|
+
return Response.json(data)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Admin route — secret key only
|
|
213
|
+
if (url.pathname === '/admin/users') {
|
|
214
|
+
const { data: auth, error } = await verifyAuth(req, {
|
|
215
|
+
allow: 'secret',
|
|
216
|
+
})
|
|
217
|
+
if (error) {
|
|
218
|
+
return Response.json(
|
|
219
|
+
{ message: error.message },
|
|
220
|
+
{ status: error.status },
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const supabaseAdmin = createAdminClient({
|
|
225
|
+
auth: { keyName: auth!.keyName },
|
|
226
|
+
})
|
|
227
|
+
const { data } = await supabaseAdmin.from('profiles').select()
|
|
228
|
+
return Response.json(data)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return new Response('Not found', { status: 404 })
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)
|
|
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.
|
|
239
|
+
|
|
240
|
+
For a complete guide with cookie parsing, JWKS caching, env bridging, and full framework adapters, see [ssr-frameworks.md](ssr-frameworks.md).
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
## Supabase environments (zero config)
|
|
2
|
+
|
|
3
|
+
On Supabase Platform and Local Development (CLI), all variables are auto-provisioned — no configuration needed
|
|
4
|
+
|
|
5
|
+
| Variable | Format | Description | Available in |
|
|
6
|
+
| --------------------------- | ---------------------------------- | ------------------------------------- | --------------------------------- |
|
|
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 |
|
|
13
|
+
|
|
14
|
+
## Non-Supabase environments (Node.js, Bun, Cloudflare, self-hosted)
|
|
15
|
+
|
|
16
|
+
Set these based on which auth modes your app uses:
|
|
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) |
|
|
24
|
+
|
|
25
|
+
### Minimal `.env` example
|
|
26
|
+
|
|
27
|
+
```env
|
|
28
|
+
SUPABASE_URL=https://<ref>.supabase.co
|
|
29
|
+
SUPABASE_SECRET_KEY=sb_secret_...
|
|
30
|
+
SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
|
|
31
|
+
SUPABASE_JWKS={"keys":[...]}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Plural vs singular keys
|
|
35
|
+
|
|
36
|
+
The SDK checks the plural form first (`SUPABASE_PUBLISHABLE_KEYS`), then falls back to the singular form (`SUPABASE_PUBLISHABLE_KEY`). The same applies to secret keys.
|
|
37
|
+
|
|
38
|
+
### Plural form — named keys as a JSON object
|
|
39
|
+
|
|
40
|
+
Use this when you have multiple keys for different clients (web, mobile, internal):
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc","web":"sb_publishable_web_xyz","mobile":"sb_publishable_mobile_123"}
|
|
44
|
+
SUPABASE_SECRET_KEYS={"default":"sb_secret_default_abc","internal":"sb_secret_internal_xyz"}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You can then validate against specific keys with named key syntax:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// Only accept the "web" publishable key
|
|
51
|
+
withSupabase({ allow: 'public:web' }, handler)
|
|
52
|
+
|
|
53
|
+
// Accept any secret key
|
|
54
|
+
withSupabase({ allow: 'secret:*' }, handler)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Singular form — equivalent to a single "default" key
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
|
|
61
|
+
SUPABASE_SECRET_KEY=sb_secret_default_abc
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is equivalent to setting the plural form with a single `"default"` entry:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
# These two are the same:
|
|
68
|
+
SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
|
|
69
|
+
SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc"}
|
|
70
|
+
```
|
|
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.
|
|
73
|
+
|
|
74
|
+
### Priority
|
|
75
|
+
|
|
76
|
+
When both singular and plural forms are set, the plural form takes priority.
|
|
77
|
+
|
|
78
|
+
## JWKS format
|
|
79
|
+
|
|
80
|
+
`SUPABASE_JWKS` accepts two formats:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
# Standard JWKS format
|
|
84
|
+
SUPABASE_JWKS={"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]}
|
|
85
|
+
|
|
86
|
+
# Bare array (convenience)
|
|
87
|
+
SUPABASE_JWKS=[{"kty":"RSA","n":"...","e":"AQAB"}]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
When `SUPABASE_JWKS` is not set, JWT verification (`allow: 'user'`) is unavailable.
|
|
91
|
+
|
|
92
|
+
## Runtime-specific behavior
|
|
93
|
+
|
|
94
|
+
The SDK reads environment variables using this priority:
|
|
95
|
+
|
|
96
|
+
1. `Deno.env.get(name)` — Deno (including Supabase Edge Functions)
|
|
97
|
+
2. `process.env[name]` — Node.js, Bun, Cloudflare Workers (with node-compat)
|
|
98
|
+
|
|
99
|
+
### Supabase Edge Functions
|
|
100
|
+
|
|
101
|
+
Environment variables are auto-provisioned by the platform. Nothing to configure.
|
|
102
|
+
|
|
103
|
+
### Deno / Node.js / Bun
|
|
104
|
+
|
|
105
|
+
Set variables via `.env` files (with a loader like `dotenv` for Node.js) or your deployment platform's environment configuration.
|
|
106
|
+
|
|
107
|
+
### Cloudflare Workers
|
|
108
|
+
|
|
109
|
+
Cloudflare Workers don't expose `Deno.env` or `process.env` by default. Two options:
|
|
110
|
+
|
|
111
|
+
1. **Enable node-compat** in `wrangler.toml`:
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
compatibility_flags = ["nodejs_compat"]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
2. **Pass overrides** via the `env` config option:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
withSupabase(
|
|
121
|
+
{
|
|
122
|
+
allow: 'user',
|
|
123
|
+
env: {
|
|
124
|
+
url: env.SUPABASE_URL,
|
|
125
|
+
publishableKeys: { default: env.SUPABASE_PUBLISHABLE_KEY },
|
|
126
|
+
secretKeys: { default: env.SUPABASE_SECRET_KEY },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
handler,
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Using env overrides
|
|
134
|
+
|
|
135
|
+
The `env` option on `withSupabase`, `createSupabaseContext`, and core primitives lets you override auto-detected values. Partial overrides are merged with what's resolved from environment variables:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { withSupabase } from '@supabase/server'
|
|
139
|
+
|
|
140
|
+
export default {
|
|
141
|
+
fetch: withSupabase(
|
|
142
|
+
{
|
|
143
|
+
allow: 'user',
|
|
144
|
+
env: {
|
|
145
|
+
url: 'http://localhost:54321', // override just the URL
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
handler,
|
|
149
|
+
),
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Using resolveEnv directly
|
|
154
|
+
|
|
155
|
+
For manual environment resolution — useful in tests, custom setups, or debugging:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { resolveEnv } from '@supabase/server/core'
|
|
159
|
+
|
|
160
|
+
const { data: env, error } = resolveEnv()
|
|
161
|
+
if (error) {
|
|
162
|
+
console.error(`Missing config: ${error.message}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// With overrides
|
|
166
|
+
const { data: envOverridden } = resolveEnv({
|
|
167
|
+
url: 'http://localhost:54321',
|
|
168
|
+
publishableKeys: { default: 'test-key' },
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`resolveEnv` returns a `SupabaseEnv` object:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface SupabaseEnv {
|
|
176
|
+
url: string
|
|
177
|
+
publishableKeys: Record<string, string>
|
|
178
|
+
secretKeys: Record<string, string>
|
|
179
|
+
jwks: JsonWebKeySet | null
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Graceful parsing
|
|
184
|
+
|
|
185
|
+
Malformed JSON in environment variables doesn't throw — the SDK falls back to empty values:
|
|
186
|
+
|
|
187
|
+
- Malformed `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS` → empty `{}`
|
|
188
|
+
- Malformed `SUPABASE_JWKS` → `null` (JWT verification unavailable)
|
|
189
|
+
- Missing `SUPABASE_URL` → `EnvError` (this is the only hard requirement)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
## Error classes
|
|
4
|
+
|
|
5
|
+
The SDK has two error classes, both with `status` (HTTP code) and `code` (machine-readable string) properties.
|
|
6
|
+
|
|
7
|
+
### EnvError
|
|
8
|
+
|
|
9
|
+
Thrown when a required environment variable is missing or malformed. Always `status: 500` — these are server configuration issues, not client errors.
|
|
10
|
+
|
|
11
|
+
| Code | Meaning |
|
|
12
|
+
| --------------------------------- | -------------------------------------------------------------- |
|
|
13
|
+
| `MISSING_SUPABASE_URL` | `SUPABASE_URL` is not set |
|
|
14
|
+
| `MISSING_PUBLISHABLE_KEY` | Named publishable key not found in `SUPABASE_PUBLISHABLE_KEYS` |
|
|
15
|
+
| `MISSING_DEFAULT_PUBLISHABLE_KEY` | No default publishable key found |
|
|
16
|
+
| `MISSING_SECRET_KEY` | Named secret key not found in `SUPABASE_SECRET_KEYS` |
|
|
17
|
+
| `MISSING_DEFAULT_SECRET_KEY` | No default secret key found |
|
|
18
|
+
| `ENV_ERROR` | Generic environment error |
|
|
19
|
+
|
|
20
|
+
### AuthError
|
|
21
|
+
|
|
22
|
+
Thrown when authentication or authorization fails. Status is `401` for invalid credentials, `500` for server-side auth failures.
|
|
23
|
+
|
|
24
|
+
| Code | Status | Meaning |
|
|
25
|
+
| ------------------------------ | ------ | ------------------------------------------- |
|
|
26
|
+
| `INVALID_CREDENTIALS` | 401 | No credential matched any allowed auth mode |
|
|
27
|
+
| `CREATE_SUPABASE_CLIENT_ERROR` | 500 | Auth succeeded but client creation failed |
|
|
28
|
+
| `AUTH_ERROR` | 401 | Generic authentication error |
|
|
29
|
+
|
|
30
|
+
## How errors surface in each layer
|
|
31
|
+
|
|
32
|
+
Different layers of the SDK handle errors differently. Understanding which pattern each function uses prevents surprises.
|
|
33
|
+
|
|
34
|
+
| Function | Pattern | What happens on error |
|
|
35
|
+
| ------------------------- | ------------- | ------------------------------------------------------------------------ |
|
|
36
|
+
| `withSupabase()` | Auto-response | Returns `Response.json({ message, code }, { status })` with CORS headers |
|
|
37
|
+
| `createSupabaseContext()` | Result tuple | Returns `{ data: null, error: AuthError }` |
|
|
38
|
+
| `verifyAuth()` | Result tuple | Returns `{ data: null, error: AuthError }` |
|
|
39
|
+
| `verifyCredentials()` | Result tuple | Returns `{ data: null, error: AuthError }` |
|
|
40
|
+
| `resolveEnv()` | Result tuple | Returns `{ data: null, error: EnvError }` |
|
|
41
|
+
| `createContextClient()` | **Throws** | Throws `EnvError` |
|
|
42
|
+
| `createAdminClient()` | **Throws** | Throws `EnvError` |
|
|
43
|
+
| Hono `withSupabase()` | HTTPException | Throws `HTTPException` with `cause: AuthError` |
|
|
44
|
+
|
|
45
|
+
The two client factory functions (`createContextClient`, `createAdminClient`) are the only ones that throw. Everything else returns a result tuple `{ data, error }`.
|
|
46
|
+
|
|
47
|
+
## Handling errors in withSupabase
|
|
48
|
+
|
|
49
|
+
`withSupabase` handles errors automatically. If auth fails, the caller receives a JSON response:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "message": "Invalid credentials", "code": "INVALID_CREDENTIALS" }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
with the appropriate HTTP status code and CORS headers. Your handler never runs.
|
|
56
|
+
|
|
57
|
+
If you need custom error formatting, use `createSupabaseContext` instead:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createSupabaseContext } from '@supabase/server'
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
fetch: async (req: Request) => {
|
|
64
|
+
const { data: ctx, error } = await createSupabaseContext(req, {
|
|
65
|
+
allow: 'user',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (error) {
|
|
69
|
+
// Custom error format
|
|
70
|
+
return Response.json(
|
|
71
|
+
{
|
|
72
|
+
success: false,
|
|
73
|
+
error: { message: error.message, code: error.code },
|
|
74
|
+
},
|
|
75
|
+
{ status: error.status },
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { data } = await ctx!.supabase.from('todos').select()
|
|
80
|
+
return Response.json({ success: true, data })
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Handling errors in Hono
|
|
86
|
+
|
|
87
|
+
The Hono adapter throws an `HTTPException` when auth fails. Access the original `AuthError` via `.cause`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { Hono } from 'hono'
|
|
91
|
+
import { HTTPException } from 'hono/http-exception'
|
|
92
|
+
import { withSupabase } from '@supabase/server/adapters/hono'
|
|
93
|
+
|
|
94
|
+
const app = new Hono()
|
|
95
|
+
|
|
96
|
+
app.use('*', withSupabase({ allow: 'user' }))
|
|
97
|
+
|
|
98
|
+
app.onError((err, c) => {
|
|
99
|
+
if (err instanceof HTTPException && err.cause) {
|
|
100
|
+
const authError = err.cause
|
|
101
|
+
return c.json(
|
|
102
|
+
{ message: authError.message, code: authError.code },
|
|
103
|
+
err.status,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return c.json({ message: 'Internal error' }, 500)
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Handling errors in core primitives
|
|
111
|
+
|
|
112
|
+
Result-tuple functions:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { verifyAuth, resolveEnv } from '@supabase/server/core'
|
|
116
|
+
|
|
117
|
+
// verifyAuth returns { data, error }
|
|
118
|
+
const { data: auth, error } = await verifyAuth(request, { allow: 'user' })
|
|
119
|
+
if (error) {
|
|
120
|
+
return Response.json({ message: error.message }, { status: error.status })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// resolveEnv returns { data, error }
|
|
124
|
+
const { data: env, error: envError } = resolveEnv()
|
|
125
|
+
if (envError) {
|
|
126
|
+
console.error(`Config issue [${envError.code}]: ${envError.message}`)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Client factories throw — wrap them in try/catch:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import {
|
|
134
|
+
verifyAuth,
|
|
135
|
+
createContextClient,
|
|
136
|
+
createAdminClient,
|
|
137
|
+
} from '@supabase/server/core'
|
|
138
|
+
import { EnvError } from '@supabase/server'
|
|
139
|
+
|
|
140
|
+
const { data: auth, error } = await verifyAuth(request, { allow: 'user' })
|
|
141
|
+
// ... handle error ...
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const supabase = createContextClient({ auth: { token: auth!.token } })
|
|
145
|
+
const supabaseAdmin = createAdminClient()
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (e instanceof EnvError) {
|
|
148
|
+
console.error(`Config issue [${e.code}]: ${e.message}`)
|
|
149
|
+
return Response.json({ message: e.message }, { status: 500 })
|
|
150
|
+
}
|
|
151
|
+
throw e
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Using the Errors factory map
|
|
156
|
+
|
|
157
|
+
The `Errors` object provides factory functions for creating error instances by code. Useful when building custom error handling or testing:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import {
|
|
161
|
+
Errors,
|
|
162
|
+
MissingSupabaseURLError,
|
|
163
|
+
InvalidCredentialsError,
|
|
164
|
+
} from '@supabase/server'
|
|
165
|
+
|
|
166
|
+
// Create specific errors
|
|
167
|
+
const envError = Errors[MissingSupabaseURLError]()
|
|
168
|
+
// → EnvError { message: "SUPABASE_URL is required but not set", code: "MISSING_SUPABASE_URL", status: 500 }
|
|
169
|
+
|
|
170
|
+
const authError = Errors[InvalidCredentialsError]()
|
|
171
|
+
// → AuthError { message: "Invalid credentials", code: "INVALID_CREDENTIALS", status: 401 }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Checking error types
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { AuthError, EnvError } from '@supabase/server'
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// ... some operation
|
|
181
|
+
} catch (e) {
|
|
182
|
+
if (e instanceof AuthError) {
|
|
183
|
+
// e.status is 401 or 500
|
|
184
|
+
// e.code is 'INVALID_CREDENTIALS', 'CREATE_SUPABASE_CLIENT_ERROR', or 'AUTH_ERROR'
|
|
185
|
+
}
|
|
186
|
+
if (e instanceof EnvError) {
|
|
187
|
+
// e.status is always 500
|
|
188
|
+
// e.code is one of the MISSING_* constants or 'ENV_ERROR'
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|