@supabase/server 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ # Getting Started
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ # Deno (import directly)
7
+ import { withSupabase } from 'npm:@supabase/server'
8
+
9
+ # npm
10
+ npm install @supabase/server
11
+
12
+ # pnpm
13
+ pnpm add @supabase/server
14
+
15
+ ```
16
+
17
+ `@supabase/server` requires `@supabase/supabase-js` as a peer dependency:
18
+
19
+ ```bash
20
+ # npm
21
+ npm install @supabase/supabase-js
22
+
23
+ # pnpm
24
+ pnpm add @supabase/supabase-js
25
+ ```
26
+
27
+ ## Your first authenticated endpoint
28
+
29
+ The fastest way to get a working authenticated endpoint:
30
+
31
+ ```ts
32
+ import { withSupabase } from '@supabase/server'
33
+
34
+ export default {
35
+ fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
36
+ const { data } = await ctx.supabase.from('todos').select()
37
+ return Response.json(data)
38
+ }),
39
+ }
40
+ ```
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.
43
+
44
+ This single wrapper does four things for every request:
45
+
46
+ 1. **CORS** — handles `OPTIONS` preflight and adds CORS headers to all responses
47
+ 2. **Auth** — extracts and verifies credentials from request headers
48
+ 3. **Clients** — creates two Supabase clients: one scoped to the caller, one admin
49
+ 4. **Errors** — returns a JSON error response (`{ message, code }`) if auth fails
50
+
51
+ Your handler only runs when auth succeeds.
52
+
53
+ ## A public endpoint (no auth)
54
+
55
+ ```ts
56
+ import { withSupabase } from '@supabase/server'
57
+
58
+ export default {
59
+ fetch: withSupabase({ allow: 'always' }, async (_req, _ctx) => {
60
+ return Response.json({ status: 'ok', time: new Date().toISOString() })
61
+ }),
62
+ }
63
+ ```
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`:
66
+ >
67
+ > ```toml
68
+ > [functions.my-function]
69
+ > verify_jwt = false
70
+ > ```
71
+
72
+ ## What's in the context
73
+
74
+ Every handler receives a `SupabaseContext` with these fields:
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. |
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.
86
+
87
+ The `supabaseAdmin` client always bypasses RLS. Use it for operations that need full database access regardless of who's calling.
88
+
89
+ `userClaims` gives you a lightweight view of the user's identity from the JWT. For the full Supabase `User` object (email confirmation, providers, etc.), call `ctx.supabase.auth.getUser()`.
90
+
91
+ ## Using createSupabaseContext directly
92
+
93
+ When you need the context without the full wrapper — inside a framework route handler, custom middleware, or any situation where you want to control the response yourself:
94
+
95
+ ```ts
96
+ import { createSupabaseContext } from '@supabase/server'
97
+
98
+ export default {
99
+ fetch: async (req: Request) => {
100
+ const { data: ctx, error } = await createSupabaseContext(req, {
101
+ allow: 'user',
102
+ })
103
+
104
+ if (error) {
105
+ return Response.json(
106
+ { message: error.message, code: error.code },
107
+ { status: error.status },
108
+ )
109
+ }
110
+
111
+ const { data } = await ctx!.supabase.from('todos').select()
112
+ return Response.json(data)
113
+ },
114
+ }
115
+ ```
116
+
117
+ `createSupabaseContext` returns a result tuple `{ data, error }` instead of producing a Response. This gives you full control over error formatting and response headers.
118
+
119
+ ## CORS configuration
120
+
121
+ CORS is enabled by default with standard supabase-js headers. You can customize or disable it:
122
+
123
+ ```ts
124
+ // Custom CORS headers
125
+ withSupabase(
126
+ {
127
+ allow: 'user',
128
+ cors: {
129
+ 'Access-Control-Allow-Origin': 'https://myapp.com',
130
+ 'Access-Control-Allow-Headers': 'authorization, content-type',
131
+ },
132
+ },
133
+ handler,
134
+ )
135
+
136
+ // Disable CORS (e.g., when a framework handles it)
137
+ withSupabase({ allow: 'user', cors: false }, handler)
138
+ ```
139
+
140
+ ## Runtimes
141
+
142
+ `withSupabase` and `createSupabaseContext` work with any runtime that supports the Web API `Request`/`Response` standard. The [core primitives](core-primitives.md) go further — they work in any environment where you can extract headers, regardless of the request/response model (Express, Fastify, etc.).
143
+
144
+ - **Supabase Edge Functions** — environment variables are automatically injected by the platform. Zero config needed.
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.
147
+ - **Cloudflare Workers** — enable `nodejs_compat` or pass env overrides via the `env` config option.
148
+
149
+ For full details on environment setup per runtime, see [environment-variables.md](environment-variables.md).
@@ -0,0 +1,185 @@
1
+ # Hono Adapter
2
+
3
+ ## Setup
4
+
5
+ Install Hono as a peer dependency:
6
+
7
+ ```bash
8
+ pnpm add hono
9
+ ```
10
+
11
+ The adapter exports its own `withSupabase` that returns Hono middleware instead of a fetch handler.
12
+
13
+ ## Basic app with auth
14
+
15
+ ```ts
16
+ import { Hono } from 'hono'
17
+ import { withSupabase } from '@supabase/server/adapters/hono'
18
+
19
+ const app = new Hono()
20
+
21
+ // Apply auth to all routes
22
+ app.use('*', withSupabase({ allow: 'user' }))
23
+
24
+ app.get('/todos', async (c) => {
25
+ const { supabase } = c.var.supabaseContext
26
+ const { data } = await supabase.from('todos').select()
27
+ return c.json(data)
28
+ })
29
+
30
+ app.get('/profile', async (c) => {
31
+ const { supabase, userClaims } = c.var.supabaseContext
32
+ const { data } = await supabase
33
+ .from('profiles')
34
+ .select()
35
+ .eq('id', userClaims!.id)
36
+ return c.json(data)
37
+ })
38
+
39
+ export default { fetch: app.fetch }
40
+ ```
41
+
42
+ The context is stored in `c.var.supabaseContext` and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `claims`, and `authType`.
43
+
44
+ ## Per-route auth
45
+
46
+ Apply different auth modes to different routes by using the middleware inline:
47
+
48
+ ```ts
49
+ import { Hono } from 'hono'
50
+ import { withSupabase } from '@supabase/server/adapters/hono'
51
+
52
+ const app = new Hono()
53
+
54
+ // Public route — no auth
55
+ app.get('/health', (c) => c.json({ status: 'ok' }))
56
+
57
+ // User-authenticated route
58
+ app.get('/todos', withSupabase({ allow: 'user' }), async (c) => {
59
+ const { supabase } = c.var.supabaseContext
60
+ const { data } = await supabase.from('todos').select()
61
+ return c.json(data)
62
+ })
63
+
64
+ // Secret-key-protected admin route
65
+ app.post('/admin/sync', withSupabase({ allow: 'secret' }), async (c) => {
66
+ const { supabaseAdmin } = c.var.supabaseContext
67
+ const { data } = await supabaseAdmin
68
+ .from('audit_log')
69
+ .insert({ action: 'sync' })
70
+ return c.json(data)
71
+ })
72
+
73
+ // Dual auth — users or services
74
+ app.get('/reports', withSupabase({ allow: ['user', 'secret'] }), async (c) => {
75
+ const { supabase, authType } = c.var.supabaseContext
76
+ return c.json({ authType })
77
+ })
78
+
79
+ export default { fetch: app.fetch }
80
+ ```
81
+
82
+ ## Skip behavior
83
+
84
+ If a previous middleware already set `c.var.supabaseContext`, subsequent `withSupabase` calls skip auth. This enables a pattern where route-level middleware overrides the app-wide default:
85
+
86
+ ```ts
87
+ const app = new Hono()
88
+
89
+ // App-wide: require user auth
90
+ app.use('*', withSupabase({ allow: 'user' }))
91
+
92
+ // This route needs secret auth instead.
93
+ // The route-level middleware runs first, sets the context,
94
+ // and the app-wide middleware skips.
95
+ app.post('/webhook', withSupabase({ allow: 'secret' }), async (c) => {
96
+ const { supabaseAdmin } = c.var.supabaseContext
97
+ // ...
98
+ })
99
+ ```
100
+
101
+ ## CORS
102
+
103
+ The Hono adapter does not handle CORS — the `cors` option is excluded from its config type. Use Hono's built-in CORS middleware:
104
+
105
+ ```ts
106
+ import { Hono } from 'hono'
107
+ import { cors } from 'hono/cors'
108
+ import { withSupabase } from '@supabase/server/adapters/hono'
109
+
110
+ const app = new Hono()
111
+
112
+ app.use('*', cors())
113
+ app.use('*', withSupabase({ allow: 'user' }))
114
+
115
+ app.get('/todos', async (c) => {
116
+ const { supabase } = c.var.supabaseContext
117
+ const { data } = await supabase.from('todos').select()
118
+ return c.json(data)
119
+ })
120
+
121
+ export default { fetch: app.fetch }
122
+ ```
123
+
124
+ ## Error handling
125
+
126
+ When auth fails, the adapter throws a Hono `HTTPException`. The original `AuthError` is available via `cause`:
127
+
128
+ ```ts
129
+ import { Hono } from 'hono'
130
+ import { HTTPException } from 'hono/http-exception'
131
+ import { withSupabase } from '@supabase/server/adapters/hono'
132
+ import { AuthError } from '@supabase/server'
133
+
134
+ const app = new Hono()
135
+
136
+ app.use('*', withSupabase({ allow: 'user' }))
137
+
138
+ // Custom error handler
139
+ app.onError((err, c) => {
140
+ if (err instanceof HTTPException && err.cause instanceof AuthError) {
141
+ const authError = err.cause
142
+ return c.json(
143
+ { error: authError.message, code: authError.code },
144
+ authError.status as 401 | 500,
145
+ )
146
+ }
147
+ return c.json({ error: 'Internal server error' }, 500)
148
+ })
149
+
150
+ app.get('/todos', async (c) => {
151
+ const { supabase } = c.var.supabaseContext
152
+ const { data } = await supabase.from('todos').select()
153
+ return c.json(data)
154
+ })
155
+
156
+ export default { fetch: app.fetch }
157
+ ```
158
+
159
+ ## Environment overrides
160
+
161
+ Pass `env` to override auto-detected environment variables, same as the main wrapper:
162
+
163
+ ```ts
164
+ app.use(
165
+ '*',
166
+ withSupabase({
167
+ allow: 'user',
168
+ env: { url: 'http://localhost:54321' },
169
+ }),
170
+ )
171
+ ```
172
+
173
+ ## Supabase client options
174
+
175
+ Forward options to the underlying `createClient()` calls:
176
+
177
+ ```ts
178
+ app.use(
179
+ '*',
180
+ withSupabase({
181
+ allow: 'user',
182
+ supabaseOptions: { db: { schema: 'api' } },
183
+ }),
184
+ )
185
+ ```
@@ -0,0 +1,82 @@
1
+ # Security
2
+
3
+ This document explains the security decisions behind `@supabase/server`. It's informational — you don't need to read this to use the package, but it helps if you want to understand why things work the way they do.
4
+
5
+ ## Timing-safe credential comparison
6
+
7
+ API keys are compared using constant-time comparison to prevent [timing attacks](https://en.wikipedia.org/wiki/Timing_attack).
8
+
9
+ A naive string comparison (`===`) short-circuits on the first mismatched character. An attacker can measure response times to guess the key one character at a time. With enough requests, this leaks the full key.
10
+
11
+ The package uses a **double-HMAC technique**: both strings are HMAC'd with a random ephemeral key, then the resulting digests are compared byte-by-byte with a constant-time XOR loop. This ensures that comparison time is independent of where (or whether) the strings differ.
12
+
13
+ This applies to:
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
17
+
18
+ See `src/core/utils/timing-safe-equal.ts` for the implementation.
19
+
20
+ ## Auth mode security model
21
+
22
+ Each auth mode provides a different level of trust:
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 |
30
+
31
+ Key implications:
32
+
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).
36
+
37
+ ## Named key isolation
38
+
39
+ Instead of accepting any valid API key, you can restrict an endpoint to a specific named key:
40
+
41
+ ```ts
42
+ // Accepts any secret key
43
+ withSupabase({ allow: 'secret' }, handler)
44
+
45
+ // Only accepts the "automations" secret key
46
+ withSupabase({ allow: 'secret:automations' }, handler)
47
+ ```
48
+
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.
50
+
51
+ ## JWT verification
52
+
53
+ JWT verification in `user` mode works as follows:
54
+
55
+ 1. The `Authorization: Bearer <token>` header is extracted from the request
56
+ 2. The token is verified against the JWKS from the `SUPABASE_JWKS` environment variable
57
+ 3. Verification uses `jose`'s `jwtVerify` with a **local** key set — there are no network calls to a JWKS endpoint
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.user` and `ctx.claims`
60
+
61
+ If JWKS is not configured (`SUPABASE_JWKS` is missing or malformed), `user` mode is unavailable and will always reject requests.
62
+
63
+ ## CORS handling
64
+
65
+ `withSupabase` handles CORS automatically:
66
+
67
+ - **Preflight requests** (`OPTIONS`) return `204` with CORS headers and skip the handler entirely — no auth check runs
68
+ - **All other requests** get CORS headers appended to the response
69
+ - **Error responses** (auth failures) also include CORS headers, so the browser can read the error
70
+
71
+ CORS defaults come from `@supabase/supabase-js/cors`. You can pass custom headers or disable CORS entirely with `cors: false`.
72
+
73
+ The Hono adapter does **not** handle CORS — use Hono's built-in `cors` middleware instead.
74
+
75
+ ## Credential extraction
76
+
77
+ Credentials are extracted from two standard headers:
78
+
79
+ - `Authorization: Bearer <token>` → used by `user` mode
80
+ - `apikey: <value>` → used by `public` and `secret` modes
81
+
82
+ Extraction is a separate step from verification (`extractCredentials` vs `verifyCredentials`). This separation means you can inspect raw credentials in custom flows without triggering validation.