Server: withOAuthProtectedResource

Alpha. Wraps a request handler with OAuth 2.1 Protected Resource behavior (RFC 9728).

The metadata route is matched on the path suffix, so any GET or OPTIONS ending in /oauth-protected-resource is answered here and never reaches the inner handler, at any depth. Other methods pass through.

Zero-config on Supabase Edge Functions. Elsewhere OAuthProtectedResourceConfig.resourceServer is required and OAuthProtectedResourceConfig.authorizationServer falls back to SUPABASE_URL; each throws an EnvError when it cannot be resolved.

Contributes ctx.oauthProtectedResource (the resolved metadata URL) to the downstream context. Nested under withSupabase, the key is typed on the handler's ctx when the outermost call is anchored with satisfies FetchHandler — see withSupabase's type note.

The OAuth Protected Resource surface is alpha — the config shape, the contributed context key, and the metadata route may change in a minor release.

Examples

Supabase Edge Functions — zero config

import { withOAuthProtectedResource, withSupabase } from '@supabase/server'

Deno.serve(
  withOAuthProtectedResource(
    withSupabase({ auth: 'user' }, async (_req, { supabase }) => {
      const { data, error } = await supabase.from('items').select('*')
      if (error) throw error
      return Response.json(data)
    }),
  ),
)

Any other backend

import { withOAuthProtectedResource, fromSupabaseUrl } from '@supabase/server'

export default {
  fetch: withOAuthProtectedResource(
    {
      resourceServer: (req) => new URL(req.url).origin + '/api/mcp',
      authorizationServer: fromSupabaseUrl('https://abc123.supabase.co'),
    },
    handler,
  ),
}

A non-Supabase authorization server

withOAuthProtectedResource(
  {
    resourceServer: 'https://api.example.com/mcp',
    authorizationServer: 'https://example.clerk.accounts.dev',
  },
  handler,
)