@supabase/server 0.2.0-rc.46 → 1.0.0-rc.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +93 -92
  2. package/dist/adapters/h3/index.cjs +3 -3
  3. package/dist/adapters/h3/index.d.cts +3 -3
  4. package/dist/adapters/h3/index.d.mts +3 -3
  5. package/dist/adapters/h3/index.mjs +3 -3
  6. package/dist/adapters/hono/index.cjs +2 -2
  7. package/dist/adapters/hono/index.d.cts +2 -2
  8. package/dist/adapters/hono/index.d.mts +2 -2
  9. package/dist/adapters/hono/index.mjs +2 -2
  10. package/dist/core/index.cjs +1 -1
  11. package/dist/core/index.d.cts +25 -9
  12. package/dist/core/index.d.mts +25 -9
  13. package/dist/core/index.mjs +1 -1
  14. package/dist/{create-supabase-context-C_8SbO5w.cjs → create-supabase-context-B-2NDJhL.cjs} +10 -9
  15. package/dist/{create-supabase-context-DXD5rxi1.mjs → create-supabase-context-BBZtr3D2.mjs} +10 -9
  16. package/dist/{errors-Dyj5Cjt6.d.cts → errors-0dbzn5gA.d.mts} +1 -1
  17. package/dist/{errors-m42mkqhD.d.mts → errors-CZFEYnV_.d.cts} +1 -1
  18. package/dist/index.cjs +3 -3
  19. package/dist/index.d.cts +5 -5
  20. package/dist/index.d.mts +5 -5
  21. package/dist/index.mjs +3 -3
  22. package/dist/{types-DKe8uOwI.d.mts → types-B2yXZjmG.d.mts} +40 -23
  23. package/dist/{types-DqhOaSlC.d.cts → types-u7fYLtzC.d.cts} +40 -23
  24. package/dist/{verify-auth-C4zqDlfj.cjs → verify-auth-BKZK83Y8.cjs} +66 -34
  25. package/dist/{verify-auth-CxFZy9rl.mjs → verify-auth-CZQd36s0.mjs} +66 -34
  26. package/docs/adapters/h3.md +180 -0
  27. package/docs/{hono-adapter.md → adapters/hono.md} +14 -25
  28. package/docs/api-reference.md +28 -15
  29. package/docs/auth-modes.md +38 -34
  30. package/docs/core-primitives.md +13 -13
  31. package/docs/environment-variables.md +17 -17
  32. package/docs/error-handling.md +4 -4
  33. package/docs/getting-started.md +17 -17
  34. package/docs/security.md +15 -15
  35. package/docs/ssr-frameworks.md +148 -172
  36. package/docs/typescript-generics.md +6 -6
  37. package/package.json +5 -3
  38. package/skills/supabase-server/SKILL.md +51 -44
@@ -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({ allow: 'user' }, async (_req, ctx) => {
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-adapter.md) or [core primitives](core-primitives.md) with your framework of choice.
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({ allow: 'always' }, async (_req, _ctx) => {
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 `allow: 'public'`, `allow: 'secret'`, or `allow: 'always'`, disable the platform-level JWT check in `supabase/config.toml`:
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 | 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
- | `claims` | `JWTClaims \| null` | Raw JWT payload (snake_case). `null` for non-user auth. |
82
- | `authType` | `Allow` | Which auth mode matched: `'user'`, `'public'`, `'secret'`, or `'always'`. |
83
- | `authKeyName` | `string \| null` | Which auth key name of the API key that was used. |
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 `authType` is `'user'`, the client is scoped to that user's permissions. For other auth modes, it's initialized as anonymous.
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
- allow: 'user',
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
- allow: 'user',
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({ allow: 'user', cors: false }, handler)
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-adapter.md) or [core primitives](core-primitives.md) to integrate with any framework.
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** (`allow: 'public'`) — compares the `apikey` header against stored publishable keys
16
- - **Secret key verification** (`allow: 'secret'`) — compares the `apikey` header against stored secret keys
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 | 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
- | `public` | 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
- | `always` | Nothing — all requests are accepted | Unknown | Row-Level Security | Full access |
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
- - **`public` and `secret` modes** compare the `apikey` header against known keys. The comparison is timing-safe. If you use named keys (`allow: 'secret:automations'`), only that specific key is accepted — this follows the principle of least privilege.
35
- - **`always` mode** performs zero authentication. The handler runs for every request. The `supabaseAdmin` client is still available, so a compromised `always` 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).
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({ allow: 'secret' }, handler)
43
+ withSupabase({ auth: 'secret' }, handler)
44
44
 
45
45
  // Only accepts the "automations" secret key
46
- withSupabase({ allow: 'secret:automations' }, handler)
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.claims`
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. `allow: ['user', 'public']`), 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 `'always'`) from being silently downgraded to a less-privileged auth mode. Requests that simply omit the `Authorization` header still fall through as expected.
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 `public` and `secret` modes
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.
@@ -1,141 +1,77 @@
1
- # SSR Frameworks
1
+ # Cookie-based environments (Next.js, SvelteKit, Remix)
2
2
 
3
3
  ## When you need this
4
4
 
5
- In SSR frameworks like Next.js, Nuxt, SvelteKit, and Remix, the user's JWT doesn't arrive in an `Authorization` header — it's stored in session cookies managed by `@supabase/ssr`. The high-level wrappers (`withSupabase`, `createSupabaseContext`) expect a standard `Request` with auth headers, so they don't work directly in SSR contexts.
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
- Instead, use the [core primitives](core-primitives.md) to build a lightweight adapter for your framework. The pattern is the same everywhere only the cookie-reading part changes.
7
+ The recommended pattern is to **compose `@supabase/server` with [`@supabase/ssr`](https://github.com/supabase/ssr)**:
8
8
 
9
- ## The pattern
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
- Every SSR adapter follows these steps:
12
+ You hand `@supabase/ssr`'s fresh access token to `verifyCredentials`, then build typed clients from the result.
12
13
 
13
- 1. **Extract the access token from cookies** (framework-specific)
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
- ## Reading Supabase session cookies
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` stores the session in cookies using a chunked, base64-encoded format:
22
+ ## Step 1 — `@supabase/ssr` middleware (refresh-token rotation)
23
23
 
24
- - **Cookie name:** `sb-<project-ref>-auth-token` (the project ref is extracted from your Supabase URL)
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
- const BASE64_PREFIX = 'base64-'
32
-
33
- function getAccessTokenFromCookies(
34
- getCookie: (name: string) => string | undefined,
35
- supabaseUrl: string,
36
- ): string | null {
37
- // Extract project ref from URL: "https://abc123.supabase.co" → "abc123"
38
- const ref = new URL(supabaseUrl).hostname.split('.')[0]
39
- const storageKey = `sb-${ref}-auth-token`
40
-
41
- // Try single cookie first, then chunked
42
- let raw = getCookie(storageKey) ?? null
43
-
44
- if (!raw) {
45
- const chunks: string[] = []
46
- for (let i = 0; ; i++) {
47
- const chunk = getCookie(`${storageKey}.${i}`)
48
- if (!chunk) break
49
- chunks.push(chunk)
50
- }
51
- if (chunks.length > 0) raw = chunks.join('')
52
- }
53
-
54
- if (!raw) return null
55
-
56
- // Decode base64url if needed
57
- let decoded = raw
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
- function resolveEnvFromFramework(): Partial<SupabaseEnv> {
90
- // Example: Next.js uses NEXT_PUBLIC_* for client-exposed vars
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
- ## JWKS resolution
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
- 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`.
66
+ If you skip this middleware, the cookie's access token will eventually expire and `verifyCredentials` will reject the request.
132
67
 
133
- ## Complete example: Next.js adapter
68
+ ## Step 2 composed adapter
134
69
 
135
- A full adapter for Next.js App Router works in Server Components, Server Actions, and Route Handlers:
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
- AllowWithKey,
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: { allow?: AllowWithKey | AllowWithKey[] } = { allow: 'user' },
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 { data: null, error: new Error('Missing SUPABASE_URL') }
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 token = getAccessTokenFromCookies(cookieStore, nextEnv.url)
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
- { allow: options.allow ?? 'user', env },
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
- claims: auth!.claims,
259
- authType: auth!.authType,
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({ allow: 'always' })
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({ allow: ['user', 'always'] })
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 reads cookies (`await cookies()` from `next/headers`). To adapt for another framework, replace the cookie-reading logic:
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.get(name)` in `+page.server.ts` or `+server.ts`
327
- - **Nuxt:** `useCookie(name)` in server routes, or `getCookie(event, name)` from `h3`
328
- - **Remix:** `request.headers.get('cookie')` then parse with a cookie library
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>({ allow: 'user' }, async (_req, ctx) => {
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
- allow: 'user',
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, { allow: 'user' })
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({ allow: 'user' }))
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
- allow: 'user',
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
- allow: 'user',
131
+ auth: 'user',
132
132
  supabaseOptions: {
133
133
  db: { schema: 'api' },
134
134
  global: {