• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 18325096862

07 Oct 2025 08:19PM UTC coverage: 77.538% (+0.2%) from 77.292%
18325096862

push

github

web-flow
chore: remove deprecated code (#813)

* chore: remove deprecated code

* test: fix tests

* docs: fix example apps not building

7 of 17 new or added lines in 6 files covered. (41.18%)

5851 of 7546 relevant lines covered (77.54%)

24.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.88
/Sources/Supabase/SupabaseClient.swift
1
import ConcurrencyExtras
2
import Foundation
3
import HTTPTypes
4
import IssueReporting
5

6
#if canImport(FoundationNetworking)
7
  import FoundationNetworking
8
#endif
9

10
/// Supabase Client.
11
public final class SupabaseClient: Sendable {
12
  let options: SupabaseClientOptions
13
  let supabaseURL: URL
14
  let supabaseKey: String
15
  let storageURL: URL
16
  let databaseURL: URL
17
  let functionsURL: URL
18

19
  private let _auth: AuthClient
20

21
  /// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
22
  public var auth: AuthClient {
8✔
23
    if options.auth.accessToken != nil {
8✔
24
      reportIssue(
1✔
25
        """
1✔
26
        Supabase Client is configured with the auth.accessToken option,
1✔
27
        accessing supabase.auth is not possible.
1✔
28
        """
1✔
29
      )
1✔
30
    }
1✔
31
    return _auth
8✔
32
  }
8✔
33

34
  var rest: PostgrestClient {
1✔
35
    mutableState.withValue {
1✔
36
      if $0.rest == nil {
1✔
37
        $0.rest = PostgrestClient(
1✔
38
          url: databaseURL,
1✔
39
          schema: options.db.schema,
1✔
40
          headers: headers,
1✔
41
          logger: options.global.logger,
1✔
42
          fetch: fetchWithAuth,
1✔
43
          encoder: options.db.encoder,
1✔
44
          decoder: options.db.decoder
1✔
45
        )
1✔
46
      }
1✔
47

1✔
48
      return $0.rest!
1✔
49
    }
1✔
50
  }
1✔
51

52
  /// Supabase Storage allows you to manage user-generated content, such as photos or videos.
53
  public var storage: SupabaseStorageClient {
1✔
54
    mutableState.withValue {
1✔
55
      if $0.storage == nil {
1✔
56
        $0.storage = SupabaseStorageClient(
1✔
57
          configuration: StorageClientConfiguration(
1✔
58
            url: storageURL,
1✔
59
            headers: headers,
1✔
60
            session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth),
1✔
61
            logger: options.global.logger,
1✔
62
            useNewHostname: options.storage.useNewHostname
1✔
63
          )
1✔
64
        )
1✔
65
      }
1✔
66

1✔
67
      return $0.storage!
1✔
68
    }
1✔
69
  }
1✔
70

71
  /// Realtime client for Supabase
72
  public var realtimeV2: RealtimeClientV2 {
4✔
73
    mutableState.withValue {
4✔
74
      if $0.realtime == nil {
4✔
75
        $0.realtime = _initRealtimeClient()
2✔
76
      }
2✔
77
      return $0.realtime!
4✔
78
    }
4✔
79
  }
4✔
80

81
  /// Supabase Functions allows you to deploy and invoke edge functions.
82
  public var functions: FunctionsClient {
2✔
83
    mutableState.withValue {
2✔
84
      if $0.functions == nil {
2✔
85
        $0.functions = FunctionsClient(
1✔
86
          url: functionsURL,
1✔
87
          headers: headers,
1✔
88
          region: options.functions.region,
1✔
89
          logger: options.global.logger,
1✔
90
          fetch: fetchWithAuth
1✔
91
        )
1✔
92
      }
1✔
93

2✔
94
      return $0.functions!
2✔
95
    }
2✔
96
  }
2✔
97

98
  let _headers: HTTPFields
99
  /// Headers provided to the inner clients on initialization.
100
  ///
101
  /// - Note: This collection is non-mutable, if you want to provide different headers, pass it in ``SupabaseClientOptions/GlobalOptions/headers``.
102
  public var headers: [String: String] {
8✔
103
    _headers.dictionary
8✔
104
  }
8✔
105

106
  struct MutableState {
107
    var listenForAuthEventsTask: Task<Void, Never>?
108
    var storage: SupabaseStorageClient?
109
    var rest: PostgrestClient?
110
    var functions: FunctionsClient?
111
    var realtime: RealtimeClientV2?
112

113
    var changedAccessToken: String?
114
  }
115

116
  let mutableState = LockIsolated(MutableState())
3✔
117

118
  private var session: URLSession {
×
119
    options.global.session
×
120
  }
×
121

122
  #if !os(Linux) && !os(Android)
123
    /// Create a new client.
124
    /// - Parameters:
125
    ///   - supabaseURL: The unique Supabase URL which is supplied when you create a new project in your project dashboard.
126
    ///   - supabaseKey: The unique Supabase Key which is supplied when you create a new project in your project dashboard.
127
    public convenience init(supabaseURL: URL, supabaseKey: String) {
1✔
128
      self.init(
1✔
129
        supabaseURL: supabaseURL,
1✔
130
        supabaseKey: supabaseKey,
1✔
131
        options: SupabaseClientOptions()
1✔
132
      )
1✔
133
    }
1✔
134
  #endif
135

136
  /// Create a new client.
137
  /// - Parameters:
138
  ///   - supabaseURL: The unique Supabase URL which is supplied when you create a new project in your project dashboard.
139
  ///   - supabaseKey: The unique Supabase Key which is supplied when you create a new project in your project dashboard.
140
  ///   - options: Custom options to configure client's behavior.
141
  public init(
142
    supabaseURL: URL,
143
    supabaseKey: String,
144
    options: SupabaseClientOptions
145
  ) {
3✔
146
    self.supabaseURL = supabaseURL
3✔
147
    self.supabaseKey = supabaseKey
3✔
148
    self.options = options
3✔
149

3✔
150
    storageURL = supabaseURL.appendingPathComponent("/storage/v1")
3✔
151
    databaseURL = supabaseURL.appendingPathComponent("/rest/v1")
3✔
152
    functionsURL = supabaseURL.appendingPathComponent("/functions/v1")
3✔
153

3✔
154
    _headers = HTTPFields(defaultHeaders)
3✔
155
      .merging(
3✔
156
        with: HTTPFields(
3✔
157
          [
3✔
158
            "Authorization": "Bearer \(supabaseKey)",
3✔
159
            "Apikey": supabaseKey,
3✔
160
          ]
3✔
161
        )
3✔
162
      )
3✔
163
      .merging(with: HTTPFields(options.global.headers))
3✔
164

3✔
165
    // default storage key uses the supabase project ref as a namespace
3✔
166
    let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token"
3✔
167

3✔
168
    _auth = AuthClient(
3✔
169
      url: supabaseURL.appendingPathComponent("/auth/v1"),
3✔
170
      headers: _headers.dictionary,
3✔
171
      flowType: options.auth.flowType,
3✔
172
      redirectToURL: options.auth.redirectToURL,
3✔
173
      storageKey: options.auth.storageKey ?? defaultStorageKey,
3✔
174
      localStorage: options.auth.storage,
3✔
175
      logger: options.global.logger,
3✔
176
      encoder: options.auth.encoder,
3✔
177
      decoder: options.auth.decoder,
3✔
178
      fetch: {
3✔
179
        // DON'T use `fetchWithAuth` method within the AuthClient as it may cause a deadlock.
×
180
        try await options.global.session.data(for: $0)
×
181
      },
×
182
      autoRefreshToken: options.auth.autoRefreshToken
3✔
183
    )
3✔
184

3✔
185
    if options.auth.accessToken == nil {
3✔
186
      listenForAuthEvents()
2✔
187
    }
2✔
188
  }
3✔
189

190
  /// Performs a query on a table or a view.
191
  /// - Parameter table: The table or view name to query.
192
  /// - Returns: A PostgrestQueryBuilder instance.
193
  public func from(_ table: String) -> PostgrestQueryBuilder {
×
194
    rest.from(table)
×
195
  }
×
196

197
  /// Performs a function call.
198
  /// - Parameters:
199
  ///   - fn: The function name to call.
200
  ///   - params: The parameters to pass to the function call.
201
  ///   - count: Count algorithm to use to count rows returned by the function.
202
  ///             Only applicable for set-returning functions.
203
  /// - Returns: A PostgrestFilterBuilder instance.
204
  /// - Throws: An error if the function call fails.
205
  public func rpc(
206
    _ fn: String,
207
    params: some Encodable & Sendable,
208
    count: CountOption? = nil
209
  ) throws -> PostgrestFilterBuilder {
×
210
    try rest.rpc(fn, params: params, count: count)
×
211
  }
×
212

213
  /// Performs a function call.
214
  /// - Parameters:
215
  ///   - fn: The function name to call.
216
  ///   - count: Count algorithm to use to count rows returned by the function.
217
  ///            Only applicable for set-returning functions.
218
  /// - Returns: A PostgrestFilterBuilder instance.
219
  /// - Throws: An error if the function call fails.
220
  public func rpc(
221
    _ fn: String,
222
    count: CountOption? = nil
223
  ) throws -> PostgrestFilterBuilder {
×
224
    try rest.rpc(fn, count: count)
×
225
  }
×
226

227
  /// Select a schema to query or perform an function (rpc) call.
228
  ///
229
  /// The schema needs to be on the list of exposed schemas inside Supabase.
230
  /// - Parameter schema: The schema to query.
231
  public func schema(_ schema: String) -> PostgrestClient {
×
232
    rest.schema(schema)
×
233
  }
×
234

235
  /// Returns all Realtime channels.
236
  public var channels: [RealtimeChannelV2] {
×
NEW
237
    Array(realtimeV2.channels.values)
×
238
  }
×
239

240
  /// Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
241
  /// - Parameters:
242
  ///   - name: The name of the Realtime channel.
243
  ///   - options: The options to pass to the Realtime channel.
244
  public func channel(
245
    _ name: String,
246
    options: @Sendable (inout RealtimeChannelConfig) -> Void = { _ in }
×
247
  ) -> RealtimeChannelV2 {
×
248
    realtimeV2.channel(name, options: options)
×
249
  }
×
250

251
  /// Unsubscribes and removes Realtime channel from Realtime client.
252
  /// - Parameter channel: The Realtime channel to remove.
253
  public func removeChannel(_ channel: RealtimeChannelV2) async {
×
254
    await realtimeV2.removeChannel(channel)
×
255
  }
×
256

257
  /// Unsubscribes and removes all Realtime channels from Realtime client.
258
  public func removeAllChannels() async {
×
259
    await realtimeV2.removeAllChannels()
×
260
  }
×
261

262
  /// Handles an incoming URL received by the app.
263
  ///
264
  /// ## Usage example:
265
  ///
266
  /// ### UIKit app lifecycle
267
  ///
268
  /// In your `AppDelegate.swift`:
269
  ///
270
  /// ```swift
271
  /// public func application(
272
  ///   _ application: UIApplication,
273
  ///   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
274
  /// ) -> Bool {
275
  ///   if let url = launchOptions?[.url] as? URL {
276
  ///     supabase.handle(url)
277
  ///   }
278
  ///
279
  ///   return true
280
  /// }
281
  ///
282
  /// func application(
283
  ///   _ app: UIApplication,
284
  ///   open url: URL,
285
  ///   options: [UIApplication.OpenURLOptionsKey: Any]
286
  /// ) -> Bool {
287
  ///   supabase.handle(url)
288
  ///   return true
289
  /// }
290
  /// ```
291
  ///
292
  /// ### UIKit app lifecycle with scenes
293
  ///
294
  /// In your `SceneDelegate.swift`:
295
  ///
296
  /// ```swift
297
  /// func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
298
  ///   guard let url = URLContexts.first?.url else { return }
299
  ///   supabase.handle(url)
300
  /// }
301
  /// ```
302
  ///
303
  /// ### SwiftUI app lifecycle
304
  ///
305
  /// In your `AppDelegate.swift`:
306
  ///
307
  /// ```swift
308
  /// SomeView()
309
  ///   .onOpenURL { url in
310
  ///     supabase.handle(url)
311
  ///   }
312
  /// ```
313
  public func handle(_ url: URL) {
×
314
    auth.handle(url)
×
315
  }
×
316

317
  deinit {
1✔
318
    mutableState.listenForAuthEventsTask?.cancel()
1✔
319
  }
1✔
320

321
  @Sendable
322
  private func fetchWithAuth(_ request: URLRequest) async throws -> (Data, URLResponse) {
×
323
    try await session.data(for: adapt(request: request))
×
324
  }
×
325

326
  @Sendable
327
  private func uploadWithAuth(
328
    _ request: URLRequest,
329
    from data: Data
330
  ) async throws -> (Data, URLResponse) {
×
331
    try await session.upload(for: adapt(request: request), from: data)
×
332
  }
×
333

334
  private func adapt(request: URLRequest) async -> URLRequest {
×
335
    let token = try? await _getAccessToken()
×
336

×
337
    var request = request
×
338
    if let token {
×
339
      request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
×
340
    }
×
341
    return request
×
342
  }
×
343

344
  private func _getAccessToken() async throws -> String? {
2✔
345
    if let accessToken = options.auth.accessToken {
2✔
346
      try await accessToken()
×
347
    } else {
2✔
348
      try await auth.session.accessToken
2✔
349
    }
2✔
350
  }
2✔
351

352
  private func listenForAuthEvents() {
2✔
353
    let task = Task {
2✔
354
      for await (event, session) in auth.authStateChanges {
2✔
355
        await handleTokenChanged(event: event, session: session)
2✔
356
      }
2✔
357
    }
2✔
358
    mutableState.withValue {
2✔
359
      $0.listenForAuthEventsTask = task
2✔
360
    }
2✔
361
  }
2✔
362

363
  private func handleTokenChanged(event: AuthChangeEvent, session: Session?) async {
2✔
364
    let accessToken: String? = mutableState.withValue {
2✔
365
      if [.initialSession, .signedIn, .tokenRefreshed].contains(event),
2✔
366
        $0.changedAccessToken != session?.accessToken
2✔
367
      {
2✔
368
        $0.changedAccessToken = session?.accessToken
×
369
        return session?.accessToken ?? supabaseKey
×
370
      }
2✔
371

2✔
372
      if event == .signedOut {
2✔
373
        $0.changedAccessToken = nil
×
374
        return supabaseKey
×
375
      }
2✔
376

2✔
377
      return nil
2✔
378
    }
2✔
379

2✔
380
    await realtimeV2.setAuth(accessToken)
2✔
381
  }
2✔
382

383
  private func _initRealtimeClient() -> RealtimeClientV2 {
2✔
384
    var realtimeOptions = options.realtime
2✔
385
    realtimeOptions.headers.merge(with: _headers)
2✔
386

2✔
387
    if realtimeOptions.logger == nil {
2✔
388
      realtimeOptions.logger = options.global.logger
2✔
389
    }
2✔
390

2✔
391
    if realtimeOptions.accessToken == nil {
2✔
392
      realtimeOptions.accessToken = { [weak self] in
2✔
393
        try await self?._getAccessToken()
2✔
394
      }
×
395
    } else {
2✔
396
      reportIssue(
×
397
        """
×
398
        You assigned a custom `accessToken` closure to the RealtimeClientV2. This might not work as you expect
×
399
        as SupabaseClient uses Auth for pulling an access token to send on the realtime channels.
×
400

×
401
        Please make sure you know what you're doing.
×
402
        """
×
403
      )
×
404
    }
×
405

2✔
406
    return RealtimeClientV2(
2✔
407
      url: supabaseURL.appendingPathComponent("/realtime/v1"),
2✔
408
      options: realtimeOptions
2✔
409
    )
2✔
410
  }
2✔
411
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc