feat: implement server-side DX primitives, wrappers, and adapters#6
feat: implement server-side DX primitives, wrappers, and adapters#6
Conversation
Two-layer architecture: - Layer 1 (wrappers): withSupabase, beforeUserCreated, afterUserCreated, withWebhookAuth - Layer 2 (core): verifyAuth, verifyCredentials, extractCredentials, createContextClient, createAdminClient, resolveEnv Auth modes: always, public, secret, user (with named key support). CORS handling built into withSupabase. Hono middleware adapter. JWKS-based JWT verification via jose. 52 tests covering all modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auth hooks (beforeUserCreated, afterUserCreated) removed pending redesign. Deno import examples now use npm: prefix per internal default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…N errors Follows Hono's recommended pattern — lets app owners handle errors globally via app.onError. The original AuthError is accessible via err.cause for custom error formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Layer 1 orchestrators (createSupabaseContext, withSupabase) now live in src/ alongside the main entry point, making the two-layer architecture visible in the file structure. Adds dedicated unit tests for createSupabaseContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ES2022 lib for Error.cause support and DOM for web globals. Type Hono app with SupabaseContext variables in tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids naming clash with the supabase client inside SupabaseContext, following Hono conventions for descriptive variable names (jwtPayload, requestId, etc.). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Free Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
kallebysantos
left a comment
There was a problem hiding this comment.
I think we should add some doc comments to improve DX,
similar on how supabase-js does
Looks good to me!!
Remove NamedKey interface and store keys as plain dicts internally, matching the JSON env var shape. Update key selection semantics: bare "public"/"secret" matches only "default" key, colon syntax matches a specific named key, and wildcard "*" matches any value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements the initial @supabase/server package by replacing the placeholder with a server-side DX surface: a main wrapper (withSupabase), composable core primitives (verifyAuth, verifyCredentials, resolveEnv, etc.), a first wrapper (verifyWebhookSignature), and a Hono adapter middleware.
Changes:
- Add server-side auth/env primitives and context/client factories under
src/core, plus mainwithSupabaseandcreateSupabaseContext. - Introduce wrappers (
verifyWebhookSignature) and a Hono adapter middleware, with Vitest coverage. - Update package/build configuration for multi-entry exports (
/core,/wrappers,/adapters/hono) and add required dependencies.
Reviewed changes
Copilot reviewed 32 out of 35 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tsdown.config.ts | Builds multiple entrypoints and marks @supabase/supabase-js/hono as externals. |
| tsconfig.json | Adds DOM/ES2022 libs to support Fetch/WebCrypto types. |
| src/index.ts | Replaces placeholder export with public API exports and types. |
| src/types.ts | Defines shared types for env, auth, context, and CORS config. |
| src/errors.ts | Adds EnvError and AuthError for consistent error modeling. |
| src/env.d.ts | Adds global typings for Deno/process env access. |
| src/with-supabase.ts | Implements declarative request wrapper with auth + CORS handling. |
| src/create-supabase-context.ts | Builds SupabaseContext from request + config using core primitives. |
| src/cors.ts | Implements CORS header builder and response wrapper. |
| src/core/index.ts | Exposes core primitives via @supabase/server/core. |
| src/core/resolve-env.ts | Resolves env from runtime + overrides (URL/keys/JWKS). |
| src/core/extract-credentials.ts | Extracts bearer token and apikey from request headers. |
| src/core/verify-credentials.ts | Validates auth modes (always/public/secret/user) incl. JWT verification. |
| src/core/verify-auth.ts | Convenience wrapper: request → credentials → verification. |
| src/core/utils/timing-safe-equal.ts | Provides timing-safe string comparison helper. |
| src/core/create-context-client.ts | Creates RLS-scoped Supabase client (anon or user token). |
| src/core/create-admin-client.ts | Creates admin Supabase client (service role). |
| src/wrappers/webhook.ts | Adds webhook signature verification helper. |
| src/wrappers/index.ts | Exposes wrappers via @supabase/server/wrappers. |
| src/adapters/hono/middleware.ts | Adds Hono middleware to inject SupabaseContext into c.var. |
| src/adapters/hono/index.ts | Exposes Hono adapter via @supabase/server/adapters/hono. |
| *.test.ts files | Adds Vitest coverage for wrappers/core/context/CORS/Hono adapter. |
| package.json | Renames package, adds subpath exports, peers, deps, and test scripts. |
| pnpm-lock.yaml | Locks new dependencies (jose, hono, @supabase/supabase-js, vitest). |
| README.md | Documents new API surface, usage examples, and exports. |
| CONTRIBUTING.md | Renames package references to @supabase/server. |
| CHANGELOG.md | Adjusts formatting of existing entries. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const jwkSet = createLocalJWKSet(env.jwks) | ||
| const { payload } = await jwtVerify(credentials.token, jwkSet) | ||
| const claims = payload as unknown as JWTClaims | ||
| return { | ||
| authType: 'user', | ||
| token: credentials.token, | ||
| user: claimsToUser(claims), | ||
| claims, | ||
| } |
There was a problem hiding this comment.
jwtVerify payload is cast to JWTClaims without runtime validation. If a token is missing sub (or has non-string fields), claimsToUser can produce user.id === undefined despite the UserIdentity type requiring a string. Add minimal runtime checks (e.g., ensure payload.sub is a non-empty string) and fail verification when required claims are absent.
| const anonKey = resolved.publishableKeys['default'] ?? '' | ||
|
|
||
| return createClient(resolved.url, anonKey, { | ||
| global: { | ||
| headers: token ? { Authorization: `Bearer ${token}` } : {}, |
There was a problem hiding this comment.
Falling back to an empty anon key ('') will create a Supabase client that fails later with confusing auth errors. Consider throwing an EnvError (or returning a typed error) when the required publishable key is missing, so callers get an immediate, actionable failure.
| const { data: resolved, error } = resolveEnv(env) | ||
| if (error) throw error | ||
|
|
||
| const secretKey = resolved.secretKeys['default'] ?? '' |
There was a problem hiding this comment.
Falling back to an empty service role key ('') will create an admin client that fails later with confusing 401/403 errors. Consider throwing an EnvError (or returning a typed error) when the required secret key is missing, so misconfiguration is caught early.
| const secretKey = resolved.secretKeys['default'] ?? '' | |
| const secretKey = resolved.secretKeys['default'] | |
| if (!secretKey) { | |
| throw new Error('Missing Supabase service role key "default" in environment configuration') | |
| } |
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.mts", | ||
| "import": "./dist/index.mjs", | ||
| "require": "./dist/index.cjs" | ||
| }, |
There was a problem hiding this comment.
The exports map points types to .d.mts files, but the package-level types field points to ./dist/index.d.cts. If the build doesn't emit both variants consistently, consumers can get missing/incorrect typings depending on resolution mode. Consider aligning on one generated type filename scheme (and ensuring subpath type entries match it).
| const origin = Array.isArray(origins) ? origins.join(', ') : origins | ||
|
|
||
| const headers: Record<string, string> = { | ||
| 'Access-Control-Allow-Origin': origin, |
There was a problem hiding this comment.
Access-Control-Allow-Origin cannot be a comma-separated list of origins. Joining an origins array into a single header value will cause browsers to reject CORS. Consider either (a) only accepting a single origin string / "*", or (b) changing the API to accept the request origin and reflect a single matched origin (and set Vary: Origin).
| const origin = Array.isArray(origins) ? origins.join(', ') : origins | |
| const headers: Record<string, string> = { | |
| 'Access-Control-Allow-Origin': origin, | |
| const originHeader = | |
| Array.isArray(origins) ? (origins[0] ?? '*') : origins | |
| const headers: Record<string, string> = { | |
| 'Access-Control-Allow-Origin': originHeader, |
| if (opts.maxAge != null) { | ||
| headers['Access-Control-Max-Age'] = String(opts.maxAge) | ||
| } | ||
|
|
||
| if (opts.credentials) { |
There was a problem hiding this comment.
When credentials: true, CORS forbids Access-Control-Allow-Origin: *. The current logic can emit Allow-Credentials: true with a wildcard origin, which browsers will reject. You likely need to echo a concrete origin (and add Vary: Origin) or disable credentials when origin is *.
| if (opts.maxAge != null) { | |
| headers['Access-Control-Max-Age'] = String(opts.maxAge) | |
| } | |
| if (opts.credentials) { | |
| if (origin !== '*') { | |
| headers['Vary'] = 'Origin' | |
| } | |
| if (opts.maxAge != null) { | |
| headers['Access-Control-Max-Age'] = String(opts.maxAge) | |
| } | |
| if (opts.credentials && origin !== '*') { |
… config Import CORS headers from @supabase/supabase-js/cors as defaults. Replace CorsConfig interface with plain Record<string, string> for custom headers. Remove buildCorsHeaders/addCorsHeaders from public exports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Throw EnvError when default publishable/secret key missing - Wrap client creation in try-catch in createSupabaseContext - Validate JWT sub claim is a string before casting - Guard empty key name in parseAllowMode - Convert empty Bearer token to null in extractCredentials - Add tests for timingSafeEqual, client creators, and missing-key error path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sets the right expectation that ctx.userClaims is JWT-derived identity, not the full Supabase User object. JSDoc points to supabase.auth.getUser() for the complete User. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d CORS - extractCredentials: empty Bearer, whitespace, case sensitivity - verifyCredentials: trailing colon, multiple colons, wildcard with empty keys - Hono middleware: skip when context already set - CORS: overwrite existing headers behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
What kind of change does this PR introduce?
Initial implementation of the server-side DX package including core primitives, declarative wrappers, and framework adapters.
What is the current behavior?
Package is a placeholder. None implementations yet.
What is the new behavior?
@supabase/serverwithSupabase,createSupabaseContext@supabase/server/coreverifyAuth,verifyCredentials,extractCredentials,createContextClient,createAdminClient,resolveEnv@supabase/server/wrappersverifyWebhookSignature(auth hooks planned)@supabase/server/adapters/honoMain export — what 90% of developers reach for. Includes the two Layer 1 entry points:
withSupabase(declarative wrapper) andcreateSupabaseContext(direct context creation)./core— Layer 2 composable primitives. For power users, framework adapters, and teams building domain-specific wrappers (e.g. MCP)./wrappers— first-party specialized wrappers for Supabase integration points (auth hooks, database webhooks, storage events)./adapters— framework-specific middleware. Hono first, others as needed.Additional context
More details on implementation document.