This is the official Supabase SDK for Swift, mirroring the design of supabase-js.
Sources/: Source code organized by moduleAuth/: Authentication moduleFunctions/: Edge Functions clientPostgREST/: Database clientPostgrestMacros/: The@Tablemacros. An opt-in product:Supabasedoes not re-export it, so only apps thatimport PostgrestMacroscompile swift-syntaxPostgrestMacrosPlugin/: The compiler plugin that implements the macrosRealtime/: Realtime subscriptionsStorage/: File storage clientSupabase/: Main client that integrates all modulesHelpers/: Shared utilities, including the HTTP layerTestHelpers/: Test utilities
Tests/: Unit and integration tests organized by module, plus:CompileFailures/: Fixtures that must fail to compile (./scripts/check-compile-failures.sh)SupabaseTypegen*OutputTests/: Compile and run code fromsupabase-typegen(see tools/supabase-typegen/README.md)
Examples/: Example applications demonstrating SDK usagetools/:supabase-typegen(a separate SwiftPM package) and the Node tooling for spell checksdocs/migrations/: Migration guides.docs/migrations/v<N>/holds one file per module for major version<N>; the rootV<N>_MIGRATION.mdis its overview
Package.swift lists the dependencies and says which target uses each one. It
serves Swift 6.4+, and Package@swift-6.2.swift serves earlier
toolchains. Keep the two in sync.
The main SupabaseClient acts as a facade for all sub-clients (Auth, Database, Storage, Functions, Realtime). Each sub-client can also be used independently, with init(configuration:). CODE_STYLE.md "Client Configuration" sets the shape of every configuration type.
The SDK is fully async/await based. Avoid using completion handlers in new code.
- A public call that changes client state is
asyncand returns once the change has taken effect. A caller who does not want to wait wraps it in aTask. - Every async API returns promptly when its task is cancelled, and it throws
CancellationError(see CODE_STYLE.md "The error model"). - Observation uses streams, not callbacks (see CODE_STYLE.md "Streams and Events").
- Stateful public clients are an
actoror afinal class: Sendable. Mutable state sits behindLockIsolatedor an internal actor. Do not addNSLock,Mutex, or Combine. - Use
@unchecked Sendableonly for a Foundation bridge, with a comment that says why. - Mark blocking I/O
@concurrent. Decode user types on the caller's actor, so isolated conformances work. - Name every long-lived task
supabase.<module>.<role>. - Helpers owns one app-lifecycle observer. Each module maps its events to its own policy, and the setting is
observesAppLifecycle.
All public types should conform to Sendable where appropriate for Swift 6 compatibility.
- Every client module (Auth, Functions, PostgREST, Realtime, Storage) has
@_exported import Helpers, so every public Helpers type shows up through each of them. Helpers' public surface holds only types that two or more modules use in public signatures: the HTTP layer,JSONValue,SupabaseError,HTTPErrorResponse, andPageSequence. A type that belongs to one module lives in that module. - Use
packageonly for something another SDK module needs. Tests use@testable import. @_spi(Experimental)is the only SPI name.
Uses modern HTTPTypes for request/response handling. Every module shares one public seam: a ClientTransport performs the exchange and an ordered ClientMiddleware chain runs in front of it, grouped into one HTTPClientConfiguration that every client takes as a single http: parameter, so a custom networking stack, extra headers, or a test stub can be injected once for the whole SDK.
Chain. Every client builds its HTTPClient the same way. The order, outermost first: retry, user middlewares, SDK middlewares (trace context, then the facade's Auth middleware), logger, transport. No module reads the access token outside this chain, so every retry gets a fresh token.
Headers. Each request starts from the module's configured headers. These always include X-Client-Info, built by one Helpers function, and apikey, from the shared HTTPField.Name.apikey. A per-call header wins over a configured header, and a configured header wins over an SDK default. A static Authorization header wins over any token middleware in every module.
Access token. Middleware is the one way to put a token on an HTTP request. HTTP modules have no accessToken setting. A standalone client gets a token from an AccessTokenMiddleware that the caller adds to http.middlewares. Through the facade, the SDK adds its Auth middleware instead. Both run inside the retry. A provider failure is <Module>Error(kind: .accessToken) in every module, standalone and through the facade. Realtime is the one exception: the socket needs the token outside any HTTP request (join, token refresh), so RealtimeClient.Configuration.accessToken is its only token source, and httpSend uses it too. Do not add AccessTokenMiddleware to Realtime's http.
Retries. Only retryEnabled turns retry on or off. Every retry goes through RetryRequestInterceptor on the injected clock. Each module's policy (statuses, methods, attempts) is listed in the table below. A 429 is retried only with Retry-After, the same way in every module that retries.
| Module | Methods | Statuses | Attempts | Backoff (base, cap) |
|---|---|---|---|---|
| Auth | GET |
408, 429, 500, 502–504, 520–524, 530 | 3 | 500 ms, 20 s |
| PostgREST | GET, HEAD |
503, 520 | 4 | 1 s, 30 s |
| Storage | GET, HEAD, OPTIONS, and the listing calls (POST) |
408, 429, 500, 502–504, 520–524, 530 | 3 | 500 ms, 20 s |
| Functions | none | none | 1 | none |
| Realtime httpSend | none | none | 1 | none |
Every module that retries also retries the transient URLErrors in RetryRequestInterceptor.retryableURLErrorCodes. Every other Auth request (token, sign-out, OTP, and so on) is retried only after a connection failure in connectionFailureURLErrorCodes, never after a response: the server cannot have seen it. A Retry-After wait is honored up to the cap. A request with a single-pass body is never retried.
Timeouts. A parameter named timeout always means the idle timeout, as in URLRequest.timeoutInterval, and a timeout fails as .transport. A total deadline needs a different name.
Errors and cancellation. HTTPClient maps a cancelled task to CancellationError, for the response head and the body. It keeps at most the first 1 MiB of a non-2xx body. Modules map only the remaining URLErrors to <Module>Error(kind: .transport).
- Every SDK logger comes from
supabaseDefaultLogger(label: "io.supabase.<module>"), and the configuration init sets thesystemmetadata. The logger is non-optional. - A redacting handler in Helpers wraps every SDK logger when the client is created. It removes JWTs, URL query strings and fragments, and secret JSON keys (tokens, passwords, signed URLs). This is the target; SDK-2876 tracks it. Today only Auth redacts, and only in
Configuration.init, so a logger set on the configuration after init is not wrapped. - HTTP traffic logs at
.trace. At.error, log an SDK-written summary, never a rawURLErroror a message that can carry a token.
The facade takes one SupabaseClient.Configuration, with one nested type per sub-client and Global for shared settings. Client setup types end in Configuration; Options is only for per-call settings (see CODE_STYLE.md "Client Configuration"). Every module client has the same shape: a nested Configuration that holds url, a public configuration property, and init(configuration:) as the only init.
try SupabaseClient(
supabaseURL: url,
supabaseKey: key,
configuration: SupabaseClient.Configuration(
database: .init(...),
auth: .init(...),
global: .init(...)
)
)The facade passes shared settings down the same way to every sub-client:
- Each nested sub-client configuration can override
http,logger, andclock. The facade uses the module value if it is set, otherwise the global one. - One helper merges the HTTP stack for all five sub-clients: the override transport, then the global middlewares, then the module middlewares, then the SDK middlewares.
- A per-module value wins over a global value. The facade never drops or overwrites a value the user set.