This document describes the breaking changes you need to be aware of when upgrading to v3 of the
Supabase Swift SDK, together with the steps required to migrate your code. All modules
(Auth, Storage, Realtime, PostgREST, Functions, Supabase) are covered here.
Note
v3 has not been released yet. This document is updated as breaking changes land on main, so
treat it as the running list rather than the final one.
verifyOTP and its overloads return a new VerifyOTPResponse type instead of AuthResponse.
GoTrue's /verify endpoint returns a body with only { msg, code } for the first of the two
confirmations required by a secure email change — a shape AuthResponse can't represent, since it
only ever holds a Session or a User. That made verifyOTP(type: .emailChange) throw a
DecodingError instead of completing successfully. AuthResponse is also the return type of
signUp and the Passkey methods, neither of which can ever produce that shape, so growing it to
fit verifyOTP would have meant every caller of those unrelated methods handling a case only one
specific call can trigger.
VerifyOTPResponse only has the two shapes verifyOTP can actually return:
public enum VerifyOTPResponse {
case session(Session)
case emailChangeConfirmationPending(EmailChangeConfirmation)
}EmailChangeConfirmation carries the message/code GoTrue sends for that first confirmation.
There's no bare-User case here — succeeding at /verify always means either a session was
issued or the other email still needs to confirm; unlike signUp, it can never leave you with a
user and no session.
This is a compile error, not a silent behavior change: the return type itself changed, so every call site using the result needs updating.
// Before
let response: AuthResponse = try await client.verifyOTP(tokenHash: hash, type: .emailChange)
let email = response.user.email
// After
let response: VerifyOTPResponse = try await client.verifyOTP(tokenHash: hash, type: .emailChange)
switch response {
case .session(let session):
let email = session.user.email
case .emailChangeConfirmationPending(let confirmation):
print("\(confirmation.message) (code: \(confirmation.code))")
}AuthResponse itself is unchanged: signUp and the Passkey methods still return it, and user is
still non-optional there, since neither endpoint can produce the confirmation-pending shape.
Every API that carried an @available(*, deprecated, ...) annotation ahead of v3 has now been
removed outright. If your project still built without deprecation warnings, none of this affects
you. If it built with warnings, each warning's replacement (already given in the deprecation
message) is now mandatory. This is a compile error everywhere: the old symbols no longer exist.
| Before | After |
|---|---|
GoTrueClient |
AuthClient |
GoTrueMFA |
AuthMFA |
GoTrueLocalStorage |
AuthLocalStorage |
GoTrueMetaSecurity |
AuthMetaSecurity |
GoTrueError |
AuthError |
JSONEncoder.goTrue |
AuthClient.Configuration.jsonEncoder |
JSONDecoder.goTrue |
AuthClient.Configuration.jsonDecoder |
MFAEnrollParams |
MFATotpEnrollParams or MFAPhoneEnrollParams |
AuthAdmin.deleteUser(id: String, shouldSoftDelete:) |
AuthAdmin.deleteUser(id: UUID, shouldSoftDelete:) |
AuthError.sessionNotFound |
AuthError.sessionMissing |
AuthError.pkce(_:) / AuthError.PKCEFailureReason |
AuthError.pkceGrantCodeExchange(message:error:code:) |
AuthError.invalidImplicitGrantFlowURL |
AuthError.implicitGrantRedirect(message:) |
AuthError.api(_ error: APIError) / AuthError.APIError |
AuthError.api(message:errorCode:underlyingData:underlyingResponse:) |
UserAttributes.emailChangeToken |
(removed, no replacement — was unused by GoTrue) |
Also removed, with no replacement, because they no longer represent something GoTrue can throw:
AuthError.missingExpClaim, AuthError.malformedJWT, AuthError.missingURL,
AuthError.invalidRedirectScheme.
UserCredentials was deprecated ("access will be removed on the next major release") and is now
internal — it was only ever used by AuthClient itself to encode the request body for
signIn(email:password:)/signIn(phone:password:)/session refresh, never something callers were
meant to construct directly. Those signIn methods are unaffected; only direct use of the
UserCredentials type itself no longer compiles.
Customizing Auth's JSON encoding/decoding is no longer supported at all: the
AuthClient.Configuration.init/AuthClient.init overloads taking encoder:/decoder:
parameters, AuthClient.Configuration.encoder/.decoder, and
SupabaseClientOptions.AuthOptions.encoder/.decoder have all been removed. Auth always uses its
internal JSON encoder/decoder now.
// Before
let client = AuthClient(
url: url, localStorage: storage, encoder: myEncoder, decoder: myDecoder,
fetch: { try await URLSession.shared.data(for: $0) }
)
// After
let client = AuthClient(url: url, localStorage: storage)| Before | After |
|---|---|
URLQueryRepresentable |
PostgrestFilterValue |
PostgrestFilterValue.queryValue |
PostgrestFilterValue.rawValue |
.like(_:value:) |
.like(_:pattern:) |
.ilike(_:value:) |
.ilike(_:pattern:) |
.in(_:value:) |
.in(_:values:) |
.plfts(_:query:config:) |
.textSearch(_:query:config:type: .plain) |
.phfts(_:query:config:) |
.textSearch(_:query:config:type: .phrase) |
.wfts(_:query:config:) |
.textSearch(_:query:config:type: .websearch) |
.explain(...format: String) |
.explain(...format: ExplainFormat), e.g. format: .json |
The PostgrestClient.Configuration.init/PostgrestClient.init overloads taking encoder:/
decoder: were also removed, for the same reason as Auth above — customizing PostgREST's JSON
codec is no longer supported.
// Before
try await client.from("users").select().ilike("email", value: "john%").execute()
// After
try await client.from("users").select().ilike("email", pattern: "john%").execute()| Before | After |
|---|---|
BucketOptions.public / init(public:...) |
BucketOptions.isPublic / init(isPublic:...) |
createSignedURL/createSignedURLs/getPublicURL(..., download: Bool) |
download: DownloadBehavior? (.withOriginalName, .named("file.pdf")) |
createSignedURLs(...) -> [URL] |
createSignedURLs(...) -> [SignedURLResult] |
upload/update/uploadToSignedURL(...) -> String |
the overloads returning FileUploadResponse / SignedURLUploadResponse |
SortBy.init(column:order: String?) |
SortBy.init(column:order: SortOrder?) |
TransformOptions.init(...resize: String?..., format: String?) |
TransformOptions.init(...resize: ResizeMode?..., format: ImageFormat?) |
JSONEncoder.defaultStorageEncoder / JSONDecoder.defaultStorageDecoder |
(removed, no public replacement — was only ever the client's internal default) |
StorageClientConfiguration.init(...encoder:decoder:session:...) |
StorageClientConfiguration.init(...logger:...) |
Storage.File / Storage.FormData |
MultipartFormData |
// Before
let bucket = try await storage.createBucket("avatars", options: .init(public: true))
let url = try await storage.from("avatars").createSignedURL(path: "a.png", expiresIn: 60, download: true)
// After
let bucket = try await storage.createBucket("avatars", options: .init(isPublic: true))
let url = try await storage.from("avatars").createSignedURL(
path: "a.png", expiresIn: 60, download: .withOriginalName
)SupabaseClient.database and SupabaseClient.realtime have been removed.
// Before
try await supabase.database.from("users").select().execute()
supabase.realtime.connect()
// After
try await supabase.from("users").select().execute()
supabase.realtimeV2.connect()The entire legacy v1 API has been removed: RealtimeClient, RealtimeChannel, Presence, and
their supporting types (PhoenixTransport, Push, Delegated, HeartbeatTimer, TimeoutTimer,
and the Message typealias). Use RealtimeClientV2, RealtimeChannelV2, and PresenceV2 — see
the RealtimeV2 migration guide for the full
v1-to-v2 walkthrough.
RealtimeClientV2 and RealtimeChannelV2 also had their own deprecated compatibility members
removed:
| Before | After |
|---|---|
RealtimeClientV2.subscriptions |
RealtimeClientV2.channels |
RealtimeClientV2.Configuration |
RealtimeClientOptions |
RealtimeClientV2.Status |
RealtimeClientStatus |
RealtimeClientV2.init(config:) |
RealtimeClientV2.init(url:options:) |
RealtimeClientV2.addChannel(_:) |
(removed — the client tracks channels automatically) |
RealtimeChannelV2.Subscription |
RealtimeSubscription |
RealtimeChannelV2.Status |
RealtimeChannelStatus |
RealtimeChannelV2.subscribe() |
RealtimeChannelV2.subscribeWithError() |
RealtimeChannelV2.updateAuth(jwt:) |
RealtimeClientV2.setAuth(_:) |
postgresChange(_:schema:table:filter: String?:select:) |
postgresChange(_:schema:table:filter: RealtimePostgresFilter?:select:) |
broadcast(event:) -> AsyncStream<JSONObject> |
broadcastStream(event:) |
RealtimeMessageV2.eventType |
inspect the raw event value in RealtimeMessageV2.event instead |
RealtimeMessageV2.EventType.tokenExpired |
now returned as .system; check the payload instead |
ObservationToken.remove() has been removed — use .cancel() instead. PostgrestError.detail
and PostgrestError.init(detail:hint:code:message:) have been removed — use .details and
init(details:hint:code:message:).