@supabase/server 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
package/docs/getting-started.md
CHANGED
|
@@ -32,14 +32,14 @@ The fastest way to get a working authenticated endpoint:
|
|
|
32
32
|
import { withSupabase } from '@supabase/server'
|
|
33
33
|
|
|
34
34
|
export default {
|
|
35
|
-
fetch: withSupabase({
|
|
35
|
+
fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
|
|
36
36
|
const { data } = await ctx.supabase.from('todos').select()
|
|
37
37
|
return Response.json(data)
|
|
38
38
|
}),
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
> The `export default { fetch }` pattern is the standard module worker interface supported by Deno (including Supabase Edge Functions), Bun, and Cloudflare Workers. For Node.js, use the [Hono adapter](hono
|
|
42
|
+
> The `export default { fetch }` pattern is the standard module worker interface supported by Deno (including Supabase Edge Functions), Bun, and Cloudflare Workers. For Node.js, use the [Hono adapter](adapters/hono.md) or [core primitives](core-primitives.md) with your framework of choice.
|
|
43
43
|
|
|
44
44
|
This single wrapper does four things for every request:
|
|
45
45
|
|
|
@@ -56,13 +56,13 @@ Your handler only runs when auth succeeds.
|
|
|
56
56
|
import { withSupabase } from '@supabase/server'
|
|
57
57
|
|
|
58
58
|
export default {
|
|
59
|
-
fetch: withSupabase({
|
|
59
|
+
fetch: withSupabase({ auth: 'none' }, async (_req, _ctx) => {
|
|
60
60
|
return Response.json({ status: 'ok', time: new Date().toISOString() })
|
|
61
61
|
}),
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
> **Supabase Edge Functions:** By default, the platform requires a valid JWT on every request. If your function uses `
|
|
65
|
+
> **Supabase Edge Functions:** By default, the platform requires a valid JWT on every request. If your function uses `auth: 'publishable'`, `auth: 'secret'`, or `auth: 'none'`, disable the platform-level JWT check in `supabase/config.toml`:
|
|
66
66
|
>
|
|
67
67
|
> ```toml
|
|
68
68
|
> [functions.my-function]
|
|
@@ -73,16 +73,16 @@ export default {
|
|
|
73
73
|
|
|
74
74
|
Every handler receives a `SupabaseContext` with these fields:
|
|
75
75
|
|
|
76
|
-
| Field | Type
|
|
77
|
-
| --------------- |
|
|
78
|
-
| `supabase` | `SupabaseClient`
|
|
79
|
-
| `supabaseAdmin` | `SupabaseClient`
|
|
80
|
-
| `userClaims` | `UserClaims \| null`
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `authKeyName` | `string \|
|
|
76
|
+
| Field | Type | Description |
|
|
77
|
+
| --------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
78
|
+
| `supabase` | `SupabaseClient` | Client scoped to the caller. RLS policies apply. |
|
|
79
|
+
| `supabaseAdmin` | `SupabaseClient` | Admin client. Bypasses RLS. |
|
|
80
|
+
| `userClaims` | `UserClaims \| null` | JWT-derived identity (`id`, `email`, `role`, `appMetadata`, `userMetadata`). `null` for non-user auth. |
|
|
81
|
+
| `jwtClaims` | `JWTClaims \| null` | Raw JWT payload (snake_case). `null` for non-user auth. |
|
|
82
|
+
| `authMode` | `AuthMode` | Which auth mode matched: `'user'`, `'publishable'`, `'secret'`, or `'none'`. |
|
|
83
|
+
| `authKeyName` | `string \| undefined` | Which auth key name of the API key that was used. Omitted for `'user'` / `'none'`. |
|
|
84
84
|
|
|
85
|
-
The `supabase` client respects Row-Level Security. When `
|
|
85
|
+
The `supabase` client respects Row-Level Security. When `authMode` is `'user'`, the client is scoped to that user's permissions. For other auth modes, it's initialized as anonymous.
|
|
86
86
|
|
|
87
87
|
The `supabaseAdmin` client always bypasses RLS. Use it for operations that need full database access regardless of who's calling.
|
|
88
88
|
|
|
@@ -98,7 +98,7 @@ import { createSupabaseContext } from '@supabase/server'
|
|
|
98
98
|
export default {
|
|
99
99
|
fetch: async (req: Request) => {
|
|
100
100
|
const { data: ctx, error } = await createSupabaseContext(req, {
|
|
101
|
-
|
|
101
|
+
auth: 'user',
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
if (error) {
|
|
@@ -124,7 +124,7 @@ CORS is enabled by default with standard supabase-js headers. You can customize
|
|
|
124
124
|
// Custom CORS headers
|
|
125
125
|
withSupabase(
|
|
126
126
|
{
|
|
127
|
-
|
|
127
|
+
auth: 'user',
|
|
128
128
|
cors: {
|
|
129
129
|
'Access-Control-Allow-Origin': 'https://myapp.com',
|
|
130
130
|
'Access-Control-Allow-Headers': 'authorization, content-type',
|
|
@@ -134,7 +134,7 @@ withSupabase(
|
|
|
134
134
|
)
|
|
135
135
|
|
|
136
136
|
// Disable CORS (e.g., when a framework handles it)
|
|
137
|
-
withSupabase({
|
|
137
|
+
withSupabase({ auth: 'user', cors: false }, handler)
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
## Runtimes
|
|
@@ -143,7 +143,7 @@ withSupabase({ allow: 'user', cors: false }, handler)
|
|
|
143
143
|
|
|
144
144
|
- **Supabase Edge Functions** — environment variables are automatically injected by the platform. Zero config needed.
|
|
145
145
|
- **Deno / Bun** — works out of the box with the module worker pattern.
|
|
146
|
-
- **Node.js** — set variables via `.env` files or your hosting platform. Use the [Hono adapter](hono
|
|
146
|
+
- **Node.js** — set variables via `.env` files or your hosting platform. Use the [Hono adapter](adapters/hono.md) or [core primitives](core-primitives.md) to integrate with any framework.
|
|
147
147
|
- **Cloudflare Workers** — enable `nodejs_compat` or pass env overrides via the `env` config option.
|
|
148
148
|
|
|
149
149
|
For full details on environment setup per runtime, see [environment-variables.md](environment-variables.md).
|
package/docs/security.md
CHANGED
|
@@ -12,8 +12,8 @@ The package uses a **double-HMAC technique**: both strings are HMAC'd with a ran
|
|
|
12
12
|
|
|
13
13
|
This applies to:
|
|
14
14
|
|
|
15
|
-
- **Publishable key verification** (`
|
|
16
|
-
- **Secret key verification** (`
|
|
15
|
+
- **Publishable key verification** (`auth: 'publishable'`) — compares the `apikey` header against stored publishable keys
|
|
16
|
+
- **Secret key verification** (`auth: 'secret'`) — compares the `apikey` header against stored secret keys
|
|
17
17
|
|
|
18
18
|
See `src/core/utils/timing-safe-equal.ts` for the implementation.
|
|
19
19
|
|
|
@@ -21,18 +21,18 @@ See `src/core/utils/timing-safe-equal.ts` for the implementation.
|
|
|
21
21
|
|
|
22
22
|
Each auth mode provides a different level of trust:
|
|
23
23
|
|
|
24
|
-
| Mode
|
|
25
|
-
|
|
|
26
|
-
| `user`
|
|
27
|
-
| `
|
|
28
|
-
| `secret`
|
|
29
|
-
| `
|
|
24
|
+
| Mode | What it verifies | Who the caller is | `supabase` client | `supabaseAdmin` client |
|
|
25
|
+
| ------------- | ----------------------------------- | ------------------------ | ------------------ | ---------------------- |
|
|
26
|
+
| `user` | JWT signature against JWKS | An authenticated user | Row-Level Security | Full access |
|
|
27
|
+
| `publishable` | Publishable API key (timing-safe) | A known client app | Row-Level Security | Full access |
|
|
28
|
+
| `secret` | Secret API key (timing-safe) | A trusted server/service | Full access | Full access |
|
|
29
|
+
| `none` | Nothing — all requests are accepted | Unknown | Row-Level Security | Full access |
|
|
30
30
|
|
|
31
31
|
Key implications:
|
|
32
32
|
|
|
33
33
|
- **`user` mode** verifies the JWT using a local JWKS (JSON Web Key Set). The token must contain a `sub` claim. Verification uses the `jose` library's `jwtVerify` with a local key set — no network calls to an auth server.
|
|
34
|
-
- **`
|
|
35
|
-
- **`
|
|
34
|
+
- **`publishable` and `secret` modes** compare the `apikey` header against known keys. The comparison is timing-safe. If you use named keys (`auth: 'secret:automations'`), only that specific key is accepted — this follows the principle of least privilege.
|
|
35
|
+
- **`none` mode** performs zero authentication. The handler runs for every request. The `supabaseAdmin` client is still available, so a compromised `none` endpoint with write operations is a security risk. Only use it for truly public endpoints or when you implement your own auth (e.g., webhook signature verification).
|
|
36
36
|
|
|
37
37
|
## Named key isolation
|
|
38
38
|
|
|
@@ -40,10 +40,10 @@ Instead of accepting any valid API key, you can restrict an endpoint to a specif
|
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
42
|
// Accepts any secret key
|
|
43
|
-
withSupabase({
|
|
43
|
+
withSupabase({ auth: 'secret' }, handler)
|
|
44
44
|
|
|
45
45
|
// Only accepts the "automations" secret key
|
|
46
|
-
withSupabase({
|
|
46
|
+
withSupabase({ auth: 'secret:automations' }, handler)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
This limits the blast radius if a key is compromised. An attacker with the `web` publishable key cannot access an endpoint that requires `secret:automations`. Named keys also make it easier to rotate or revoke access for a specific consumer without affecting others.
|
|
@@ -56,11 +56,11 @@ JWT verification in `user` mode works as follows:
|
|
|
56
56
|
2. The token is verified against the JWKS from the `SUPABASE_JWKS` environment variable
|
|
57
57
|
3. Verification uses `jose`'s `jwtVerify` with a **local** key set — there are no network calls to a JWKS endpoint
|
|
58
58
|
4. The token must contain a `sub` (subject) claim to be considered valid
|
|
59
|
-
5. On success, the decoded claims are available as `ctx.userClaims` and `ctx.
|
|
59
|
+
5. On success, the decoded claims are available as `ctx.userClaims` and `ctx.jwtClaims`
|
|
60
60
|
|
|
61
61
|
If JWKS is not configured (`SUPABASE_JWKS` is missing or malformed), `user` mode is unavailable and will always reject requests.
|
|
62
62
|
|
|
63
|
-
**No silent downgrade.** When `user` is combined with other modes (e.g. `
|
|
63
|
+
**No silent downgrade.** When `user` is combined with other modes (e.g. `auth: ['user', 'publishable']`), a JWT that is present but fails verification rejects the request with `InvalidCredentialsError` — it does not fall through to the next mode. This prevents a bad token paired with a valid `apikey` (or with `'none'`) from being silently downgraded to a less-privileged auth mode. Requests that simply omit the `Authorization` header still fall through as expected.
|
|
64
64
|
|
|
65
65
|
## CORS handling
|
|
66
66
|
|
|
@@ -79,6 +79,6 @@ The Hono adapter does **not** handle CORS — use Hono's built-in `cors` middlew
|
|
|
79
79
|
Credentials are extracted from two standard headers:
|
|
80
80
|
|
|
81
81
|
- `Authorization: Bearer <token>` → used by `user` mode
|
|
82
|
-
- `apikey: <value>` → used by `
|
|
82
|
+
- `apikey: <value>` → used by `publishable` and `secret` modes
|
|
83
83
|
|
|
84
84
|
Extraction is a separate step from verification (`extractCredentials` vs `verifyCredentials`). This separation means you can inspect raw credentials in custom flows without triggering validation.
|
package/docs/ssr-frameworks.md
CHANGED
|
@@ -1,141 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Cookie-based environments (Next.js, SvelteKit, Remix)
|
|
2
2
|
|
|
3
3
|
## When you need this
|
|
4
4
|
|
|
5
|
-
In
|
|
5
|
+
In cookie-based frameworks like Next.js, Nuxt, SvelteKit, and Remix, the user's JWT lives in session cookies rather than the `Authorization` header. The high-level wrappers (`withSupabase`, `createSupabaseContext`) expect a standard `Request` with auth headers, so they don't work directly here.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The recommended pattern is to **compose `@supabase/server` with [`@supabase/ssr`](https://github.com/supabase/ssr)**:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- `@supabase/ssr` owns the cookie session lifecycle — reads cookies, writes cookies, and handles refresh-token rotation via middleware.
|
|
10
|
+
- `@supabase/server` adds JWT verification (`verifyCredentials`), an RLS-scoped server client (`createContextClient`), and a service-role client (`createAdminClient`) on top.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
You hand `@supabase/ssr`'s fresh access token to `verifyCredentials`, then build typed clients from the result.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
2. **Bridge environment variables** to the `SupabaseEnv` shape
|
|
15
|
-
3. **Resolve JWKS** for JWT verification
|
|
16
|
-
4. **Call `verifyCredentials`** with the extracted token
|
|
17
|
-
5. **Create clients** with `createContextClient` + `createAdminClient`
|
|
18
|
-
6. **Return a `SupabaseContext`**
|
|
14
|
+
## How the pieces fit
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
1. **`@supabase/ssr` middleware** runs on every request and refreshes the access token cookie. Without it, the cookie goes stale, `verifyCredentials` rejects expired tokens, and the user appears logged out — even with a valid refresh token. (Server Components can't write cookies, which is why the refresh has to happen in middleware.)
|
|
17
|
+
2. **`@supabase/ssr` `createServerClient`** runs inside your Server Component / Route Handler, reads the (now-fresh) cookie, and exposes `auth.getSession()` / `auth.getUser()`.
|
|
18
|
+
3. **`verifyCredentials`** from `@supabase/server/core` cryptographically verifies that access token against JWKS and returns the parsed claims.
|
|
19
|
+
4. **`createContextClient`** builds an RLS-scoped `supabase-js` client bound to the verified token.
|
|
20
|
+
5. **`createAdminClient`** builds a service-role client (no token needed).
|
|
21
21
|
|
|
22
|
-
`@supabase/ssr`
|
|
22
|
+
## Step 1 — `@supabase/ssr` middleware (refresh-token rotation)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
- **Chunking:** if the session is too large for a single cookie, it's split into `sb-<ref>-auth-token.0`, `.1`, `.2`, etc.
|
|
26
|
-
- **Base64 encoding:** the cookie value may be prefixed with `base64-`, indicating base64url encoding
|
|
27
|
-
|
|
28
|
-
To extract the access token:
|
|
24
|
+
This middleware is required. It refreshes the access token cookie before any Server Component or Route Handler runs:
|
|
29
25
|
|
|
30
26
|
```ts
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (decoded.startsWith(BASE64_PREFIX)) {
|
|
59
|
-
try {
|
|
60
|
-
const base64 = decoded
|
|
61
|
-
.substring(BASE64_PREFIX.length)
|
|
62
|
-
.replace(/-/g, '+')
|
|
63
|
-
.replace(/_/g, '/')
|
|
64
|
-
decoded = atob(base64)
|
|
65
|
-
} catch {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Parse the session JSON and extract access_token
|
|
71
|
-
try {
|
|
72
|
-
const session = JSON.parse(decoded)
|
|
73
|
-
return session.access_token ?? null
|
|
74
|
-
} catch {
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
The `getCookie` parameter is a function that reads a cookie by name — its implementation depends on your framework (e.g., `cookies().get(name)?.value` in Next.js, `event.cookies.get(name)` in SvelteKit).
|
|
81
|
-
|
|
82
|
-
## Environment variable bridging
|
|
83
|
-
|
|
84
|
-
SSR frameworks often use their own naming conventions for environment variables. Map them to a `Partial<SupabaseEnv>` that the core primitives expect:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import type { SupabaseEnv } from '@supabase/server'
|
|
27
|
+
// middleware.ts
|
|
28
|
+
import { createServerClient } from '@supabase/ssr'
|
|
29
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
30
|
+
|
|
31
|
+
export async function middleware(request: NextRequest) {
|
|
32
|
+
let supabaseResponse = NextResponse.next({ request })
|
|
33
|
+
|
|
34
|
+
const supabase = createServerClient(
|
|
35
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
36
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
|
37
|
+
{
|
|
38
|
+
cookies: {
|
|
39
|
+
getAll() {
|
|
40
|
+
return request.cookies.getAll()
|
|
41
|
+
},
|
|
42
|
+
setAll(cookiesToSet) {
|
|
43
|
+
cookiesToSet.forEach(({ name, value }) =>
|
|
44
|
+
request.cookies.set(name, value),
|
|
45
|
+
)
|
|
46
|
+
supabaseResponse = NextResponse.next({ request })
|
|
47
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
48
|
+
supabaseResponse.cookies.set(name, value, options),
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
)
|
|
88
54
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
92
|
-
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
93
|
-
const secretKey = process.env.SUPABASE_SECRET_KEY
|
|
55
|
+
// Triggers refresh-token rotation and writes the new cookies via setAll.
|
|
56
|
+
await supabase.auth.getUser()
|
|
94
57
|
|
|
95
|
-
return
|
|
96
|
-
url: url ?? undefined,
|
|
97
|
-
publishableKeys: publishableKey ? { default: publishableKey } : {},
|
|
98
|
-
secretKeys: secretKey ? { default: secretKey } : {},
|
|
99
|
-
// JWKS: either set SUPABASE_JWKS env var, or fetch it (see below)
|
|
100
|
-
}
|
|
58
|
+
return supabaseResponse
|
|
101
59
|
}
|
|
102
|
-
```
|
|
103
60
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
JWT verification requires a JWKS (JSON Web Key Set). Two options:
|
|
107
|
-
|
|
108
|
-
**Option 1: Set the `SUPABASE_JWKS` environment variable.** This is auto-available on the Supabase platform and in local CLI. If set, the core primitives pick it up automatically — no extra code needed.
|
|
109
|
-
|
|
110
|
-
**Option 2: Fetch from the well-known endpoint and cache.** Useful when deploying to environments where `SUPABASE_JWKS` isn't set:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import type { SupabaseEnv } from '@supabase/server'
|
|
114
|
-
|
|
115
|
-
let cachedJwks: SupabaseEnv['jwks'] = null
|
|
116
|
-
|
|
117
|
-
async function getJwks(supabaseUrl: string): Promise<SupabaseEnv['jwks']> {
|
|
118
|
-
if (cachedJwks) return cachedJwks
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const res = await fetch(`${supabaseUrl}/auth/v1/.well-known/jwks.json`)
|
|
122
|
-
if (!res.ok) return null
|
|
123
|
-
cachedJwks = await res.json()
|
|
124
|
-
return cachedJwks
|
|
125
|
-
} catch {
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
61
|
+
export const config = {
|
|
62
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
128
63
|
}
|
|
129
64
|
```
|
|
130
65
|
|
|
131
|
-
|
|
66
|
+
If you skip this middleware, the cookie's access token will eventually expire and `verifyCredentials` will reject the request.
|
|
132
67
|
|
|
133
|
-
##
|
|
68
|
+
## Step 2 — composed adapter
|
|
134
69
|
|
|
135
|
-
|
|
70
|
+
The adapter reads the (middleware-refreshed) cookie via `@supabase/ssr`, then hands the access token to `@supabase/server`'s primitives. The return shape matches the high-level `createSupabaseContext`, so callers see a familiar `{ supabase, supabaseAdmin, userClaims, jwtClaims, authMode }` bundle.
|
|
136
71
|
|
|
137
72
|
```ts
|
|
138
73
|
// lib/supabase/context.ts
|
|
74
|
+
import { createServerClient } from '@supabase/ssr'
|
|
139
75
|
import { cookies } from 'next/headers'
|
|
140
76
|
import {
|
|
141
77
|
verifyCredentials,
|
|
@@ -143,55 +79,11 @@ import {
|
|
|
143
79
|
createAdminClient,
|
|
144
80
|
} from '@supabase/server/core'
|
|
145
81
|
import type {
|
|
146
|
-
|
|
82
|
+
AuthModeWithKey,
|
|
147
83
|
SupabaseContext,
|
|
148
84
|
SupabaseEnv,
|
|
149
85
|
} from '@supabase/server'
|
|
150
86
|
|
|
151
|
-
const BASE64_PREFIX = 'base64-'
|
|
152
|
-
|
|
153
|
-
function getAccessTokenFromCookies(
|
|
154
|
-
cookieStore: Awaited<ReturnType<typeof cookies>>,
|
|
155
|
-
url: string,
|
|
156
|
-
): string | null {
|
|
157
|
-
const ref = new URL(url).hostname.split('.')[0]
|
|
158
|
-
const storageKey = `sb-${ref}-auth-token`
|
|
159
|
-
|
|
160
|
-
let raw = cookieStore.get(storageKey)?.value ?? null
|
|
161
|
-
|
|
162
|
-
if (!raw) {
|
|
163
|
-
const chunks: string[] = []
|
|
164
|
-
for (let i = 0; ; i++) {
|
|
165
|
-
const chunk = cookieStore.get(`${storageKey}.${i}`)?.value
|
|
166
|
-
if (!chunk) break
|
|
167
|
-
chunks.push(chunk)
|
|
168
|
-
}
|
|
169
|
-
if (chunks.length > 0) raw = chunks.join('')
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!raw) return null
|
|
173
|
-
|
|
174
|
-
let decoded = raw
|
|
175
|
-
if (decoded.startsWith(BASE64_PREFIX)) {
|
|
176
|
-
try {
|
|
177
|
-
const base64 = decoded
|
|
178
|
-
.substring(BASE64_PREFIX.length)
|
|
179
|
-
.replace(/-/g, '+')
|
|
180
|
-
.replace(/_/g, '/')
|
|
181
|
-
decoded = atob(base64)
|
|
182
|
-
} catch {
|
|
183
|
-
return null
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const session = JSON.parse(decoded)
|
|
189
|
-
return session.access_token ?? null
|
|
190
|
-
} catch {
|
|
191
|
-
return null
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
87
|
function resolveNextEnv(): Partial<SupabaseEnv> {
|
|
196
88
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
197
89
|
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
@@ -219,25 +111,54 @@ async function getJwks(supabaseUrl: string): Promise<SupabaseEnv['jwks']> {
|
|
|
219
111
|
}
|
|
220
112
|
|
|
221
113
|
export async function createSupabaseContext(
|
|
222
|
-
options: {
|
|
114
|
+
options: { auth?: AuthModeWithKey | AuthModeWithKey[] } = { auth: 'user' },
|
|
223
115
|
): Promise<
|
|
224
116
|
{ data: SupabaseContext; error: null } | { data: null; error: Error }
|
|
225
117
|
> {
|
|
226
118
|
const nextEnv = resolveNextEnv()
|
|
227
119
|
|
|
228
|
-
if (!nextEnv.url) {
|
|
229
|
-
return {
|
|
120
|
+
if (!nextEnv.url || !nextEnv.publishableKeys?.default) {
|
|
121
|
+
return {
|
|
122
|
+
data: null,
|
|
123
|
+
error: new Error('Missing SUPABASE_URL or SUPABASE_PUBLISHABLE_KEY'),
|
|
124
|
+
}
|
|
230
125
|
}
|
|
231
126
|
|
|
127
|
+
// Read the @supabase/ssr session cookie. The middleware above has already
|
|
128
|
+
// refreshed the access token, so getSession() returns a fresh JWT.
|
|
232
129
|
const cookieStore = await cookies()
|
|
233
|
-
const
|
|
130
|
+
const ssrClient = createServerClient(
|
|
131
|
+
nextEnv.url,
|
|
132
|
+
nextEnv.publishableKeys.default,
|
|
133
|
+
{
|
|
134
|
+
cookies: {
|
|
135
|
+
getAll() {
|
|
136
|
+
return cookieStore.getAll()
|
|
137
|
+
},
|
|
138
|
+
setAll(cookiesToSet) {
|
|
139
|
+
try {
|
|
140
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
141
|
+
cookieStore.set(name, value, options),
|
|
142
|
+
)
|
|
143
|
+
} catch {
|
|
144
|
+
// Server Components can't write cookies — middleware handles it.
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const {
|
|
152
|
+
data: { session },
|
|
153
|
+
} = await ssrClient.auth.getSession()
|
|
154
|
+
const token = session?.access_token ?? null
|
|
234
155
|
|
|
235
156
|
const jwks = await getJwks(nextEnv.url)
|
|
236
157
|
const env: Partial<SupabaseEnv> = { ...nextEnv, jwks }
|
|
237
158
|
|
|
238
159
|
const { data: auth, error } = await verifyCredentials(
|
|
239
160
|
{ token, apikey: null },
|
|
240
|
-
{
|
|
161
|
+
{ auth: options.auth ?? 'user', env },
|
|
241
162
|
)
|
|
242
163
|
|
|
243
164
|
if (error) {
|
|
@@ -255,14 +176,69 @@ export async function createSupabaseContext(
|
|
|
255
176
|
supabase,
|
|
256
177
|
supabaseAdmin,
|
|
257
178
|
userClaims: auth!.userClaims,
|
|
258
|
-
|
|
259
|
-
|
|
179
|
+
jwtClaims: auth!.jwtClaims,
|
|
180
|
+
authMode: auth!.authMode,
|
|
260
181
|
},
|
|
261
182
|
error: null,
|
|
262
183
|
}
|
|
263
184
|
}
|
|
264
185
|
```
|
|
265
186
|
|
|
187
|
+
## Does this replace `@supabase/ssr`?
|
|
188
|
+
|
|
189
|
+
No. `@supabase/ssr` handles cookie-based session management for frameworks like Next.js and SvelteKit. `@supabase/server` handles stateless, header-based auth for Edge Functions, Workers, and other backend runtimes. As you can see in the Next.js example above, the composable primitives already work in SSR environments but require more setup. The two packages coexist and are not replacements for each other. Deeper integration with `@supabase/ssr` is on the roadmap.
|
|
190
|
+
|
|
191
|
+
## Environment variable bridging
|
|
192
|
+
|
|
193
|
+
SSR frameworks often use their own naming conventions for environment variables. Map them to a `Partial<SupabaseEnv>` that the core primitives expect:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import type { SupabaseEnv } from '@supabase/server'
|
|
197
|
+
|
|
198
|
+
function resolveEnvFromFramework(): Partial<SupabaseEnv> {
|
|
199
|
+
// Example: Next.js uses NEXT_PUBLIC_* for client-exposed vars
|
|
200
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
201
|
+
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
202
|
+
const secretKey = process.env.SUPABASE_SECRET_KEY
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
url: url ?? undefined,
|
|
206
|
+
publishableKeys: publishableKey ? { default: publishableKey } : {},
|
|
207
|
+
secretKeys: secretKey ? { default: secretKey } : {},
|
|
208
|
+
// JWKS: either set SUPABASE_JWKS env var, or fetch it (see below)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## JWKS resolution
|
|
214
|
+
|
|
215
|
+
JWT verification requires a JWKS (JSON Web Key Set). Two options:
|
|
216
|
+
|
|
217
|
+
**Option 1: Set the `SUPABASE_JWKS` environment variable.** This is auto-available on the Supabase platform and in local CLI. If set, the core primitives pick it up automatically — no extra code needed.
|
|
218
|
+
|
|
219
|
+
**Option 2: Fetch from the well-known endpoint and cache.** Useful when deploying to environments where `SUPABASE_JWKS` isn't set:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import type { SupabaseEnv } from '@supabase/server'
|
|
223
|
+
|
|
224
|
+
let cachedJwks: SupabaseEnv['jwks'] = null
|
|
225
|
+
|
|
226
|
+
async function getJwks(supabaseUrl: string): Promise<SupabaseEnv['jwks']> {
|
|
227
|
+
if (cachedJwks) return cachedJwks
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch(`${supabaseUrl}/auth/v1/.well-known/jwks.json`)
|
|
231
|
+
if (!res.ok) return null
|
|
232
|
+
cachedJwks = await res.json()
|
|
233
|
+
return cachedJwks
|
|
234
|
+
} catch {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The cache lives in module scope, so it persists across requests for the lifetime of the server process. For serverless environments (e.g., Vercel), the cache is per-invocation — consider using an external cache or always setting `SUPABASE_JWKS`.
|
|
241
|
+
|
|
266
242
|
## Usage
|
|
267
243
|
|
|
268
244
|
### In a Server Component
|
|
@@ -313,18 +289,18 @@ export async function GET() {
|
|
|
313
289
|
|
|
314
290
|
```ts
|
|
315
291
|
// Public endpoint — no auth required
|
|
316
|
-
const { data: ctx } = await createSupabaseContext({
|
|
292
|
+
const { data: ctx } = await createSupabaseContext({ auth: 'none' })
|
|
317
293
|
|
|
318
294
|
// Accept either user JWT or skip auth
|
|
319
|
-
const { data: ctx } = await createSupabaseContext({
|
|
295
|
+
const { data: ctx } = await createSupabaseContext({ auth: ['user', 'none'] })
|
|
320
296
|
```
|
|
321
297
|
|
|
322
298
|
## Adapting for other frameworks
|
|
323
299
|
|
|
324
|
-
The adapter above is Next.js-specific only in how it
|
|
300
|
+
The adapter above is Next.js-specific only in how it wires `@supabase/ssr`'s cookie adapter. To adapt for another framework, swap the cookie adapter you pass to `createServerClient` from `@supabase/ssr` — see `@supabase/ssr`'s framework guides for the canonical patterns:
|
|
325
301
|
|
|
326
|
-
- **SvelteKit:** `event.cookies.
|
|
327
|
-
- **
|
|
328
|
-
- **
|
|
302
|
+
- **SvelteKit:** `event.cookies.getAll()` / `event.cookies.set(name, value, options)` in `+page.server.ts` or `+server.ts`.
|
|
303
|
+
- **Remix:** parse cookies from `request.headers.get('cookie')` and emit them via `Set-Cookie` in the response.
|
|
304
|
+
- **Nuxt:** use `useCookie` / `getCookie` / `setCookie` from `h3` inside server routes.
|
|
329
305
|
|
|
330
306
|
Everything else — env bridging, JWKS fetching, `verifyCredentials`, client creation — stays the same.
|
|
@@ -21,7 +21,7 @@ import { withSupabase } from '@supabase/server'
|
|
|
21
21
|
import type { Database } from './database.types.ts'
|
|
22
22
|
|
|
23
23
|
export default {
|
|
24
|
-
fetch: withSupabase<Database>({
|
|
24
|
+
fetch: withSupabase<Database>({ auth: 'user' }, async (_req, ctx) => {
|
|
25
25
|
// ctx.supabase is SupabaseClient<Database>
|
|
26
26
|
// Fully typed: column names, return type, etc.
|
|
27
27
|
const { data } = await ctx.supabase
|
|
@@ -40,7 +40,7 @@ import { createSupabaseContext } from '@supabase/server'
|
|
|
40
40
|
import type { Database } from './database.types.ts'
|
|
41
41
|
|
|
42
42
|
const { data: ctx, error } = await createSupabaseContext<Database>(request, {
|
|
43
|
-
|
|
43
|
+
auth: 'user',
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
if (error) {
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
} from '@supabase/server/core'
|
|
62
62
|
import type { Database } from './database.types.ts'
|
|
63
63
|
|
|
64
|
-
const { data: auth } = await verifyAuth(request, {
|
|
64
|
+
const { data: auth } = await verifyAuth(request, { auth: 'user' })
|
|
65
65
|
|
|
66
66
|
const supabase = createContextClient<Database>({
|
|
67
67
|
auth: { token: auth!.token },
|
|
@@ -86,7 +86,7 @@ import type { Database } from './database.types.ts'
|
|
|
86
86
|
|
|
87
87
|
const app = new Hono()
|
|
88
88
|
|
|
89
|
-
app.use('*', withSupabase({
|
|
89
|
+
app.use('*', withSupabase({ auth: 'user' }))
|
|
90
90
|
|
|
91
91
|
app.get('/todos', async (c) => {
|
|
92
92
|
const { supabase } = c.var.supabaseContext as SupabaseContext<Database>
|
|
@@ -106,7 +106,7 @@ import type { Database } from './database.types.ts'
|
|
|
106
106
|
export default {
|
|
107
107
|
fetch: withSupabase<Database>(
|
|
108
108
|
{
|
|
109
|
-
|
|
109
|
+
auth: 'user',
|
|
110
110
|
supabaseOptions: { db: { schema: 'api' } },
|
|
111
111
|
},
|
|
112
112
|
async (_req, ctx) => {
|
|
@@ -128,7 +128,7 @@ export default {
|
|
|
128
128
|
```ts
|
|
129
129
|
withSupabase<Database>(
|
|
130
130
|
{
|
|
131
|
-
|
|
131
|
+
auth: 'user',
|
|
132
132
|
supabaseOptions: {
|
|
133
133
|
db: { schema: 'api' },
|
|
134
134
|
global: {
|