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

supabase / supabase-swift / 31181589893

07 Aug 2026 01:12PM UTC coverage: 84.239% (+0.3%) from 83.944%
31181589893

push

github

web-flow
feat(storage): add vector index and vector data operations (alpha) (#1181)

* feat(storage): add vector index and vector data operations (alpha)

Adds storage.vectors.from(_:) for bucket-scoped index management
(createIndex/getIndex/listIndexes/deleteIndex) and .index(_:) for
vector data operations (putVectors/getVectors/listVectors/
queryVectors/deleteVectors), implemented by hand on the existing
StorageApi HTTP stack, matching supabase-js's storage-js vectors
client and the supabase/storage backend's wire format.

* fix(storage): use TimeInterval for VectorIndex.creationTime

Same fix as the base branch's VectorBucket.creationTime: the wire
value is a raw UNIX timestamp (seconds), not an ISO8601 string.

* refactor(storage): make VectorBucketClient/VectorIndexClient structs over StorageApi

Same composition-over-inheritance change as StorageVectorsClient:
these now hold a StorageApi dependency passed in at init instead of
subclassing it, threaded through from(_:)/index(_:).

* chore: trigger CI

* fix(storage): serialize vector bucket integration tests

vectorBucket_CRUD and listBucketsWithPrefix share the same
test-vector-bucket name, so concurrent execution could race on
create/delete/list.

200 of 202 new or added lines in 3 files covered. (99.01%)

8696 of 10323 relevant lines covered (84.24%)

39.43 hits per line

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

97.87
/Sources/Storage/VectorBucketClient.swift
1
//
2
//  VectorBucketClient.swift
3
//  Storage
4
//
5
//  Created by Guilherme Souza on 06/08/26.
6
//
7

8
public import Foundation
9
import HTTPTypes
10

11
#if canImport(FoundationNetworking)
12
  import FoundationNetworking
13
#endif
14

15
/// A client scoped to a single vector bucket, for managing its indexes and vector data.
16
///
17
/// Obtain an instance via ``StorageVectorsClient/from(_:)``:
18
///
19
/// ```swift
20
/// let bucket = client.storage.vectors.from("documents")
21
/// try await bucket.createIndex("embeddings", dimension: 1536, distanceMetric: .cosine)
22
/// let index = bucket.index("embeddings")
23
/// ```
24
///
25
/// - Warning: Vector buckets are a public alpha feature of Supabase Storage and this API is
26
///   experimental — it may change in a breaking way, or be unavailable on your project, until it
27
///   reaches general availability. Opt in with `@_spi(Experimental) import Supabase`.
28
///
29
/// ## Topics
30
///
31
/// ### Managing indexes
32
///
33
/// - ``createIndex(_:dimension:distanceMetric:dataType:metadataConfiguration:)``
34
/// - ``getIndex(_:)``
35
/// - ``listIndexes(prefix:maxResults:nextToken:)``
36
/// - ``deleteIndex(_:)``
37
///
38
/// ### Accessing vector data
39
///
40
/// - ``index(_:)``
41
@_spi(Experimental)
42
public struct VectorBucketClient: Sendable {
43
  /// The name of the vector bucket this client operates on.
44
  public let vectorBucketName: String
45

46
  private let api: StorageApi
47

48
  init(vectorBucketName: String, api: StorageApi) {
12✔
49
    self.vectorBucketName = vectorBucketName
12✔
50
    self.api = api
12✔
51
  }
12✔
52

53
  /// Creates a new vector index within this bucket.
54
  ///
55
  /// ```swift
56
  /// try await bucket.createIndex(
57
  ///   "embeddings",
58
  ///   dimension: 1536,
59
  ///   distanceMetric: .cosine,
60
  ///   metadataConfiguration: VectorIndexMetadataConfiguration(
61
  ///     nonFilterableMetadataKeys: ["raw_text"]
62
  ///   )
63
  /// )
64
  /// ```
65
  ///
66
  /// - Warning: Experimental. See ``StorageVectorsClient``.
67
  ///
68
  /// - Parameters:
69
  ///   - indexName: A unique name for the index within this bucket. 3-63 characters: lowercase
70
  ///     letters, numbers, hyphens, and dots, starting and ending with a letter or number.
71
  ///   - dimension: The dimensionality of vectors stored in this index (e.g. `1536`).
72
  ///   - distanceMetric: The similarity metric used when querying this index.
73
  ///   - dataType: The data type of vector components. Defaults to ``VectorDataType/float32``,
74
  ///     currently the only supported value.
75
  ///   - metadataConfiguration: Configuration for which metadata keys are excluded from filtering.
76
  /// - Throws: ``StorageError`` when the API rejects the request.
77
  public func createIndex(
78
    _ indexName: String,
79
    dimension: Int,
80
    distanceMetric: VectorDistanceMetric,
81
    dataType: VectorDataType = .float32,
82
    metadataConfiguration: VectorIndexMetadataConfiguration? = nil
83
  ) async throws {
2✔
84
    try await api.execute(
2✔
85
      HTTPRequest(
2✔
86
        url: api.configuration.url.appendingPathComponent("vector/CreateIndex"),
2✔
87
        method: .post,
2✔
88
        body: JSONEncoder.unconfiguredEncoder.encode(
2✔
89
          CreateIndexBody(
2✔
90
            vectorBucketName: vectorBucketName,
2✔
91
            indexName: indexName,
2✔
92
            dataType: dataType,
2✔
93
            dimension: dimension,
2✔
94
            distanceMetric: distanceMetric,
2✔
95
            metadataConfiguration: metadataConfiguration
2✔
96
          )
2✔
97
        )
2✔
98
      )
2✔
99
    )
2✔
100
  }
1✔
101

102
  /// Retrieves metadata for an existing vector index in this bucket.
103
  ///
104
  /// ```swift
105
  /// let index = try await bucket.getIndex("embeddings")
106
  /// print(index.dimension)
107
  /// ```
108
  ///
109
  /// - Warning: Experimental. See ``StorageVectorsClient``.
110
  ///
111
  /// - Parameter indexName: The name of the index to retrieve.
112
  /// - Returns: The matching ``VectorIndex``.
113
  /// - Throws: ``StorageError`` when the API rejects the request.
114
  public func getIndex(_ indexName: String) async throws -> VectorIndex {
1✔
115
    let response: GetIndexResponseBody = try await api.execute(
1✔
116
      HTTPRequest(
1✔
117
        url: api.configuration.url.appendingPathComponent("vector/GetIndex"),
1✔
118
        method: .post,
1✔
119
        body: JSONEncoder.unconfiguredEncoder.encode(
1✔
120
          VectorBucketIndexNameBody(vectorBucketName: vectorBucketName, indexName: indexName)
1✔
121
        )
1✔
122
      )
1✔
123
    )
1✔
124
    .decoded(decoder: .supabase())
1✔
125
    return response.index
1✔
126
  }
1✔
127

128
  /// Lists the vector indexes in this bucket, optionally filtered by name prefix.
129
  ///
130
  /// Results are paginated: pass the ``ListVectorIndexesResponse/nextToken`` of a previous
131
  /// response as `nextToken` to fetch the following page.
132
  ///
133
  /// ```swift
134
  /// let page = try await bucket.listIndexes(prefix: "embeddings-")
135
  /// for index in page.indexes {
136
  ///   print(index.indexName)
137
  /// }
138
  /// ```
139
  ///
140
  /// - Warning: Experimental. See ``StorageVectorsClient``.
141
  ///
142
  /// - Parameters:
143
  ///   - prefix: Returns only indexes whose name starts with this prefix. Pass `nil` for all
144
  ///     indexes.
145
  ///   - maxResults: The maximum number of indexes to return in this page.
146
  ///   - nextToken: The pagination token from a previous response.
147
  /// - Returns: A page of indexes, plus the token for the next page when more results exist.
148
  /// - Throws: ``StorageError`` when the API rejects the request.
149
  public func listIndexes(
150
    prefix: String? = nil,
151
    maxResults: Int? = nil,
152
    nextToken: String? = nil
153
  ) async throws -> ListVectorIndexesResponse {
1✔
154
    let response: ListIndexesResponseBody = try await api.execute(
1✔
155
      HTTPRequest(
1✔
156
        url: api.configuration.url.appendingPathComponent("vector/ListIndexes"),
1✔
157
        method: .post,
1✔
158
        body: JSONEncoder.unconfiguredEncoder.encode(
1✔
159
          ListIndexesBody(
1✔
160
            vectorBucketName: vectorBucketName,
1✔
161
            prefix: prefix,
1✔
162
            maxResults: maxResults,
1✔
163
            nextToken: nextToken
1✔
164
          )
1✔
165
        )
1✔
166
      )
1✔
167
    )
1✔
168
    .decoded(decoder: .supabase())
1✔
169
    return ListVectorIndexesResponse(indexes: response.indexes, nextToken: response.nextToken)
1✔
170
  }
1✔
171

172
  /// Deletes a vector index and all of its vector data.
173
  ///
174
  /// ```swift
175
  /// try await bucket.deleteIndex("embeddings")
176
  /// ```
177
  ///
178
  /// - Warning: Experimental. See ``StorageVectorsClient``.
179
  ///
180
  /// - Parameter indexName: The name of the index to delete.
181
  /// - Throws: ``StorageError`` when the API rejects the request.
182
  public func deleteIndex(_ indexName: String) async throws {
1✔
183
    try await api.execute(
1✔
184
      HTTPRequest(
1✔
185
        url: api.configuration.url.appendingPathComponent("vector/DeleteIndex"),
1✔
186
        method: .post,
1✔
187
        body: JSONEncoder.unconfiguredEncoder.encode(
1✔
188
          VectorBucketIndexNameBody(vectorBucketName: vectorBucketName, indexName: indexName)
1✔
189
        )
1✔
190
      )
1✔
191
    )
1✔
192
  }
1✔
193

194
  /// Returns a client scoped to the given index, for reading and writing vector data.
195
  ///
196
  /// ```swift
197
  /// let index = client.storage.vectors.from("documents").index("embeddings")
198
  /// try await index.putVectors([
199
  ///   VectorEntry(key: "doc-1", data: VectorData(float32: [0.1, 0.2, 0.3]))
200
  /// ])
201
  /// ```
202
  ///
203
  /// - Warning: Experimental. See ``StorageVectorsClient``.
204
  ///
205
  /// - Parameter indexName: The name of the index.
206
  /// - Returns: A ``VectorIndexClient`` configured for the given index.
207
  public func index(_ indexName: String) -> VectorIndexClient {
7✔
208
    VectorIndexClient(
7✔
209
      vectorBucketName: vectorBucketName,
7✔
210
      indexName: indexName,
7✔
211
      api: api
7✔
212
    )
7✔
213
  }
7✔
214
}
215

216
private struct VectorBucketIndexNameBody: Encodable {
217
  var vectorBucketName: String
218
  var indexName: String
219
}
220

221
private struct CreateIndexBody: Encodable {
222
  var vectorBucketName: String
223
  var indexName: String
224
  var dataType: VectorDataType
225
  var dimension: Int
226
  var distanceMetric: VectorDistanceMetric
227
  var metadataConfiguration: VectorIndexMetadataConfiguration?
228
}
229

230
private struct ListIndexesBody: Encodable {
231
  var vectorBucketName: String
232
  var prefix: String?
233
  var maxResults: Int?
234
  var nextToken: String?
235
}
236

237
private struct GetIndexResponseBody: Decodable {
238
  var index: VectorIndex
239
}
240

241
private struct ListIndexesResponseBody: Decodable {
242
  var indexes: [VectorIndexSummary]
243
  var nextToken: String?
244
}
245

246
/// The data type of the components stored in a vector index.
247
///
248
/// ```swift
249
/// try await bucket.createIndex("embeddings", dimension: 1536, distanceMetric: .cosine, dataType: .float32)
250
/// ```
251
///
252
/// ## Topics
253
///
254
/// ### Predefined data types
255
///
256
/// - ``float32``
257
@_spi(Experimental)
258
public struct VectorDataType: RawRepresentable, Hashable, Sendable {
259
  /// The raw string value sent to the API.
260
  public let rawValue: String
261

262
  /// Creates a ``VectorDataType`` from a raw string value.
263
  ///
264
  /// - Parameter rawValue: The data type string understood by the Storage vectors API.
265
  public init(rawValue: String) { self.rawValue = rawValue }
2✔
266

267
  /// 32-bit floating point vector components. Currently the only supported data type.
268
  public static let float32 = VectorDataType(rawValue: "float32")
269
}
270

271
extension VectorDataType: ExpressibleByStringLiteral {
NEW
272
  public init(stringLiteral value: String) { self.init(rawValue: value) }
×
273
}
274

275
extension VectorDataType: Codable {
276
  public func encode(to encoder: any Encoder) throws {
2✔
277
    var container = encoder.singleValueContainer()
2✔
278
    try container.encode(rawValue)
2✔
279
  }
2✔
280

281
  public init(from decoder: any Decoder) throws {
1✔
282
    let container = try decoder.singleValueContainer()
1✔
283
    self.init(rawValue: try container.decode(String.self))
1✔
284
  }
1✔
285
}
286

287
/// The similarity metric used to rank results when querying a vector index.
288
///
289
/// ```swift
290
/// try await bucket.createIndex("embeddings", dimension: 1536, distanceMetric: .cosine)
291
/// ```
292
///
293
/// ## Topics
294
///
295
/// ### Predefined metrics
296
///
297
/// - ``cosine``
298
/// - ``euclidean``
299
/// - ``dotProduct``
300
@_spi(Experimental)
301
public struct VectorDistanceMetric: RawRepresentable, Hashable, Sendable {
302
  /// The raw string value sent to the API.
303
  public let rawValue: String
304

305
  /// Creates a ``VectorDistanceMetric`` from a raw string value.
306
  ///
307
  /// - Parameter rawValue: The distance metric string understood by the Storage vectors API.
308
  public init(rawValue: String) { self.rawValue = rawValue }
3✔
309

310
  /// Cosine similarity.
311
  public static let cosine = VectorDistanceMetric(rawValue: "cosine")
312

313
  /// Euclidean (L2) distance.
314
  public static let euclidean = VectorDistanceMetric(rawValue: "euclidean")
315

316
  /// Dot product similarity.
317
  ///
318
  /// - Note: Support depends on the underlying vector store backend.
319
  public static let dotProduct = VectorDistanceMetric(rawValue: "dotproduct")
320
}
321

322
extension VectorDistanceMetric: ExpressibleByStringLiteral {
NEW
323
  public init(stringLiteral value: String) { self.init(rawValue: value) }
×
324
}
325

326
extension VectorDistanceMetric: Codable {
327
  public func encode(to encoder: any Encoder) throws {
2✔
328
    var container = encoder.singleValueContainer()
2✔
329
    try container.encode(rawValue)
2✔
330
  }
2✔
331

332
  public init(from decoder: any Decoder) throws {
2✔
333
    let container = try decoder.singleValueContainer()
2✔
334
    self.init(rawValue: try container.decode(String.self))
2✔
335
  }
2✔
336
}
337

338
/// Configuration for which metadata keys are excluded from filtering on a vector index.
339
///
340
/// - Warning: Experimental. See ``StorageVectorsClient``.
341
@_spi(Experimental)
342
public struct VectorIndexMetadataConfiguration: Codable, Sendable, Hashable {
343
  /// Metadata keys that can be stored but not used in ``VectorIndexClient`` query filters.
344
  public var nonFilterableMetadataKeys: [String]
345

346
  /// Creates a ``VectorIndexMetadataConfiguration``.
347
  ///
348
  /// - Parameter nonFilterableMetadataKeys: Metadata keys to exclude from filtering.
349
  public init(nonFilterableMetadataKeys: [String]) {
1✔
350
    self.nonFilterableMetadataKeys = nonFilterableMetadataKeys
1✔
351
  }
1✔
352
}
353

354
/// A vector index, as returned by ``VectorBucketClient``.
355
///
356
/// - Warning: Experimental. See ``StorageVectorsClient``.
357
@_spi(Experimental)
358
public struct VectorIndex: Codable, Sendable, Hashable {
359
  /// The name of the index.
360
  public var indexName: String
361

362
  /// The name of the vector bucket this index belongs to.
363
  public var vectorBucketName: String
364

365
  /// The data type of the vector components stored in this index.
366
  public var dataType: VectorDataType
367

368
  /// The dimensionality of vectors stored in this index.
369
  public var dimension: Int
370

371
  /// The similarity metric used when querying this index.
372
  public var distanceMetric: VectorDistanceMetric
373

374
  /// Configuration for which metadata keys are excluded from filtering, if any.
375
  public var metadataConfiguration: VectorIndexMetadataConfiguration?
376

377
  /// UNIX timestamp (seconds) of when the index was created, if known.
378
  public var creationTime: TimeInterval?
379
}
380

381
/// A summary of a vector index, as returned by
382
/// ``VectorBucketClient/listIndexes(prefix:maxResults:nextToken:)``.
383
///
384
/// - Warning: Experimental. See ``StorageVectorsClient``.
385
@_spi(Experimental)
386
public struct VectorIndexSummary: Codable, Sendable, Hashable {
387
  /// The name of the index.
388
  public var indexName: String
389

390
  /// The name of the vector bucket this index belongs to.
391
  public var vectorBucketName: String
392

393
  /// UNIX timestamp (seconds) of when the index was created, if known.
394
  public var creationTime: TimeInterval?
395
}
396

397
/// A page of vector indexes, as returned by
398
/// ``VectorBucketClient/listIndexes(prefix:maxResults:nextToken:)``.
399
///
400
/// - Warning: Experimental. See ``StorageVectorsClient``.
401
@_spi(Experimental)
402
public struct ListVectorIndexesResponse: Sendable {
403
  /// The indexes in this page.
404
  public var indexes: [VectorIndexSummary]
405

406
  /// The pagination token to pass to fetch the next page, or `nil` when there are no more results.
407
  public var nextToken: String?
408
}
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