Skip to content

Extract shared code from packages into a supabase_common package #1572

Description

@spydon

Background

While auditing dependencies in the monorepo we found several pieces of code that are duplicated or near-duplicated across packages. Some duplication was even introduced by the recent dependency-removal work (e.g. the in-house retry helper now lives identically in two packages). This issue tracks consolidating that shared code into a single internal (but published) supabase_common package.

Packages under packages/: functions_client, gotrue, postgrest, realtime_client, storage_client, supabase, supabase_flutter, yet_another_json_isolate.

Linear: SDK-1281

Goal

Create a supabase_common package and move genuinely shared code into it, starting with the lowest-risk, highest-confidence items and deferring anything that would break public API.

Tier 1 — identical / near-identical, lift-and-shift (no public-API risk)

  • retry / RetryOptions — byte-for-byte identical in packages/gotrue/lib/src/retry.dart and packages/storage_client/lib/src/retry.dart. Move to one shared copy.
  • ToSnakeCase extension on Enum — byte-for-byte identical in packages/gotrue/lib/src/types/auth_response.dart (~line 106) and packages/storage_client/lib/src/types.dart (~line 469).
  • Platform detectionplatform_io.dart / platform_stub.dart plus the conditional-import trick are near-identical between packages/supabase/lib/src/ and packages/supabase_flutter/lib/src/ (flutter only adds isRunningInFlutterTest). Move with the extra flag preserved.
  • Replay subject_ReplaySubject in packages/gotrue/lib/src/gotrue_client.dart (~line 1708) and _BehaviorSubject in packages/supabase/lib/src/supabase_stream_builder.dart (~line 455) reimplement the same broadcast + latest-event-replay behavior. Unify into one ReplaySubject supporting sync, onListen/onCancel, and addStream.

Tier 2 — same shape, unify with a small parameter (low risk, some reconciliation)

  • X-Client-Info header builder — every data package builds {'X-Client-Info': '<lib>-dart/$version'} (functions_client, gotrue, postgrest, realtime_client, storage_client), and supabase / supabase_flutter share a richer platform-aware variant. Provide buildClientInfoHeader(clientName, version, {platformInfo}).
  • isSuccessStatusCode(code) => code >= 200 && code <= 299 — duplicated/inlined in gotrue/fetch.dart, storage_client/fetch.dart, functions_client/functions_client.dart, postgrest/postgrest_builder.dart.
  • FetchOptions — same two fields (headers, noResolveJson) in packages/gotrue/lib/src/types/fetch_options.dart and packages/storage_client/lib/src/types.dart, differing only in nullability/constructor style.
  • base64url / JWT / PKCE / uuid helpers — currently only in gotrue (base64url.dart, helper.dart, types/jwt.dart), so not cross-package duplication, but generic utilities worth centralizing. Moving them also removes an intra-gotrue duplication (jwt.dart ~line 294 re-implements Base64Url.decodeToBytes).

Tier 3 — breaking, belongs in v3

These are "same concept, different shape" and touch public API, so they need careful reconciliation and are tracked under the v3 umbrella (#1278).

  • Shared exception baseAuthException, StorageException, PostgrestException share a message + status + toString shape and could get a common SupabaseException base. FunctionException is the outlier (status: int, details, reasonPhrase), see Make FunctionException a sealed class for v3 #1550.
  • HTTP fetch wrappersGotrueFetch, storage Fetch, functions' inlined invoke, postgrest's _parseResponse all follow "check 2xx → decode JSON → else parse typed error" but each is bound to its own exception type. Shareable in pieces (see Tier 2), not as one wrapper.
  • RetryTimer (packages/realtime_client/lib/src/retry_timer.dart) — same exponential-backoff concept as retry but timer/callback-driven for websocket reconnect. Only the delay calculation could be shared.

Explicitly out of scope

  • version.dart (const version = '...') is auto-generated per package by release tooling and must stay local.
  • Package-specific enums/constants (RealtimeProtocolVersion, AuthChangeEvent, OtpType, etc.).
  • The Logger('supabase.<area>') convention — no code to share; a supabaseLogger(area) factory is optional polish at most (see Look into setting up logging in a proper way #1533).

Considerations

  • supabase_common must be a real published package (client packages are published independently and cannot depend on an unpublished local-only package). That adds a new versioned artifact for release tooling and a new dependency edge for each package that uses it.
  • Total shared code is currently modest (~250-300 lines for Tier 1). Weigh the packaging/publishing overhead against the duplication before committing to the full scope.

Suggested approach

  1. First PR: create supabase_common and move Tier 1 only (all identical or near-identical, no API changes).
  2. Follow-up PR(s): Tier 2.
  3. Tier 3 is breaking and tracked under v3 (☂️ v3 tracking issue #1278).

Related

The duplication was surfaced during dependency cleanup:

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions