Skip to content

feat(supabase): add plugins option to createClient#2485

Open
mandarini wants to merge 1 commit into
masterfrom
feat/createclient-plugins
Open

feat(supabase): add plugins option to createClient#2485
mandarini wants to merge 1 commit into
masterfrom
feat/createclient-plugins

Conversation

@mandarini

@mandarini mandarini commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🔍 Description

Adds a plugins option to createClient plus a co-located defineSupabasePlugin helper — the SDK leg of the Supabase Plugins initiative. Each plugin contributes one typed namespace on the client, inferred from the plugins array with no manual annotations:

import { createClient, defineSupabasePlugin } from '@supabase/supabase-js'

const guestbookPlugin = () =>
  defineSupabasePlugin({
    name: 'guestbook',
    client: (client) => ({
      list: () => client.functions.invoke('guestbook-mw', { method: 'GET' }),
    }),
  })

const supabase = createClient(url, key, { plugins: [guestbookPlugin()] })
await supabase.guestbook.list() // ✅ typed, autocompleted

Pattern siblings (same overload + type-accumulation approach, so the whole family reads the same):

  • supabase/web-middleware#9 — pipeline() + Entry (server middleware composition)
  • feat: add middleware option to withSupabase server#88middleware option on withSupabase (MiddlewareCtx accumulation; the server-side array holds a plugin's middleware, this PR's array holds its client namespaces)

What changed?

  • src/lib/plugins.ts (new) — SupabasePlugin<Name, Namespace>, recursive PluginNamespaces<Plugins>, defineSupabasePlugin() (identity fn that captures the literal name + namespace type), and attachPlugins()
  • src/index.tscreateClient becomes an overloaded function: overload 1 is byte-for-byte today's signature plus plugins?: never (forces correct overload resolution — TS skips excess-property checks during overload resolution); overload 2 infers const Plugins from options.plugins and returns SupabaseClient<...> & PluginNamespaces<Plugins>. The class's five generics are untouched
  • src/SupabaseClient.ts — plugins attach as the last constructor statement, so factories receive a fully initialized client (client.functions, client.from, earlier plugins' namespaces all usable). Read from options, not settings (same as options?.storage)
  • src/lib/types.tsplugins?: readonly AnySupabasePlugin[] on SupabaseClientOptions, so new SupabaseClient() users and @supabase/ssr wrappers get runtime behavior for free
  • Collision guard: a plugin name matching an existing client member (from, auth, functions, storage, ...) or a duplicate plugin name throws at construction
  • Tests: test/unit/plugins.test.ts (9 tests) + tsd cases in test/types/index.test-d.ts

Why was this change needed?

SDK leg of the Supabase Plugins RFC (Linear: SDK, project Supabase Plugins – Better Auth paradigm). The RFC decided defineSupabasePlugin is co-located in supabase-js — the plugins runtime hook has to live here anyway, since only the client can mount the namespace.

🔄 Breaking changes

  • This PR contains no breaking changes

Additive only (feat → minor). Overload 1 preserves the existing createClient signature exactly; all existing tsd assertions pass unchanged.

📝 Additional notes

Deliberate scope cuts (tracked in Linear SDK-1163):

  • No events bus (auth.userCreated, …) — follow-up per the RFC phasing
  • No type-level collision check (runtime throw only) — same status as ordering/collision in web-middleware

Type caveats, stated honestly:

  • Partial explicit generics: createClient<Database>(url, key, { plugins }) compiles and works at runtime, but plugin namespaces fall back to untyped (TS has no partial inference — Proposal: Partial Type Argument Inference microsoft/TypeScript#26242; server#88 has the identical latent behavior). Escape hatch documented in JSDoc: const plugins = [...] as const + pass typeof plugins as the 4th type arg. A withPlugins(client, plugins) compositional helper is a possible clean follow-up
  • TS floor: the const type-parameter modifier in the emitted d.ts raises minimum consumer TypeScript to 5.0. If that's a concern, a union-based mapped type can replace the recursive tuple — happy to switch
  • Docs-site coverage (supabase_js_v2.yml) is a separate-repo follow-up

Verification: build, test:unit (123 passed), test:types (tsd + jsr dry-run), test:exports (attw) all green. The pkg.pr.new preview from this PR will be consumed by plugin-examples to replace its hand-rolled withGuestbookPlugin client wiring — that's the end-to-end validation on a real local stack.

🤖 Generated with Claude Code

Merge order

  • Depends on: nothing — this PR is self-contained (no imports from web-middleware or server) and can merge independently.
  • supabase/web-middleware#9 and feat: add middleware option to withSupabase server#88 are pattern siblings (same flat-array design; middleware on the server, plugins here), not dependencies; the three are exercised together in plugin-examples.

Adds a plugins option to createClient plus a co-located
defineSupabasePlugin helper. Each plugin contributes one typed
namespace at supabase.<name>, inferred from the plugins array via a
recursive PluginNamespaces type — mirroring the PluginsCtx pattern from
supabase/server#88 and the Entry pattern from supabase/web-middleware#9.

- overload 1 keeps today's exact createClient signature (plugins?: never
  forces correct overload resolution); overload 2 infers the plugins
  tuple and returns SupabaseClient & PluginNamespaces<Plugins>
- namespaces attach at the end of the SupabaseClient constructor so
  plugin factories receive a fully initialized client; collisions with
  existing client members or duplicate plugin names throw
- scope: name + client namespace only; the events bus from the RFC is a
  follow-up (SDK-1163)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mandarini mandarini requested a review from a team as a code owner July 2, 2026 07:04
@github-actions github-actions Bot added the supabase-js Related to the supabase-js library. label Jul 2, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2485

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2485

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2485

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2485

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2485

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2485

commit: d9a10b7

@mandarini mandarini added the do-not-merge Do not merge this PR. label Jul 2, 2026
@mandarini mandarini self-assigned this Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Do not merge this PR. supabase-js Related to the supabase-js library.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant