Skip to content

feat: implement server-side DX primitives, wrappers, and adapters#6

Merged
tomaspozo merged 23 commits intomainfrom
feat/server-dx-v0
Mar 20, 2026
Merged

feat: implement server-side DX primitives, wrappers, and adapters#6
tomaspozo merged 23 commits intomainfrom
feat/server-dx-v0

Conversation

@tomaspozo
Copy link
Copy Markdown
Member

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?

Export What's in it
@supabase/server withSupabase, createSupabaseContext
@supabase/server/core verifyAuth, verifyCredentials, extractCredentials, createContextClient, createAdminClient, resolveEnv
@supabase/server/wrappers verifyWebhookSignature (auth hooks planned)
@supabase/server/adapters/hono Hono middleware

Main export — what 90% of developers reach for. Includes the two Layer 1 entry points: withSupabase (declarative wrapper) and createSupabaseContext (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.

tomaspozo and others added 10 commits March 5, 2026 22:27
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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 8, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Free

Run ID: 5076f3a6-9903-4d1d-8111-2495843de1f1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Note

🎁 Summarized by CodeRabbit Free

Your 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 @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Member

@kallebysantos kallebysantos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add some doc comments to improve DX,
similar on how supabase-js does

Looks good to me!!

Comment thread src/core/resolve-env.ts Outdated
Comment thread src/core/verify-credentials.test.ts
Comment thread src/core/verify-credentials.test.ts
Comment thread src/core/verify-credentials.test.ts
Comment thread src/core/verify-credentials.test.ts
Comment thread src/core/verify-credentials.test.ts
kallebysantos and others added 7 commits March 10, 2026 13:16
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>
@tomaspozo tomaspozo marked this pull request as ready for review March 20, 2026 03:49
Copilot AI review requested due to automatic review settings March 20, 2026 03:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 main withSupabase and createSupabaseContext.
  • 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.

Comment on lines +104 to +112
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,
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/core/create-context-client.ts Outdated
Comment on lines +13 to +17
const anonKey = resolved.publishableKeys['default'] ?? ''

return createClient(resolved.url, anonKey, {
global: {
headers: token ? { Authorization: `Bearer ${token}` } : {},
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/core/create-admin-client.ts Outdated
const { data: resolved, error } = resolveEnv(env)
if (error) throw error

const secretKey = resolved.secretKeys['default'] ?? ''
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const secretKey = resolved.secretKeys['default'] ?? ''
const secretKey = resolved.secretKeys['default']
if (!secretKey) {
throw new Error('Missing Supabase service role key "default" in environment configuration')
}

Copilot uses AI. Check for mistakes.
Comment thread package.json
Comment on lines 21 to 26
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread src/cors.ts Outdated
Comment on lines +11 to +14
const origin = Array.isArray(origins) ? origins.join(', ') : origins

const headers: Record<string, string> = {
'Access-Control-Allow-Origin': origin,
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment thread src/cors.ts Outdated
Comment on lines +22 to +26
if (opts.maxAge != null) {
headers['Access-Control-Max-Age'] = String(opts.maxAge)
}

if (opts.credentials) {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 *.

Suggested change
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 !== '*') {

Copilot uses AI. Check for mistakes.
tomaspozo and others added 4 commits March 19, 2026 23:31
… 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>
@tomaspozo tomaspozo merged commit d206e5c into main Mar 20, 2026
5 of 6 checks passed
@tomaspozo tomaspozo deleted the feat/server-dx-v0 branch March 20, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants