A Swift SDK for SurrealDB with full async/await support, type-safe query macros, and live query streaming.
Alpha release - this SDK is in early development and the public API is subject to breaking changes without notice.
- Swift 6.1+
- SurrealDB v3+
iOS 17+ · macOS 14+ · tvOS 17+ · watchOS 10+ · visionOS 1+
- HTTP and WebSocket transports
- Type-safe CRUD via
@SurrealModelmacro and query DSL - Live queries over WebSocket via
AsyncStream - Raw SQL queries with bound parameters
- Root, namespace, database, and record-access authentication
- Automatic WebSocket reconnection
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/surrealdb/surrealdb.swift.git", from: "0.1.0"),
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "SurrealDB", package: "surrealdb.swift")
]
)
]import SurrealDB
// 1. Define a model
@SurrealModel("person")
struct Person: Codable, Sendable {
let id: String?
let name: String
let age: Int
}
// 2. Create a client and connect
let client = try SurrealWebSocketClient(endpoint: "ws://localhost:8000")
try await client.connect()
// 3. Authenticate and select a namespace/database
_ = try await client.signin(.root(username: "root", password: "root"))
try await client.use(namespace: "myapp", database: "mydb")
// 4. Create a record
let people = try await client.create(Person(id: nil, name: "Ada", age: 30))
// 5. Query records
let results = try await client.select(Person.self, where: Person.Fields.age > 18, limit: 10)Annotate any struct, class, or actor with @SurrealModel to bind it to a SurrealDB table. The macro generates a surrealTable constant, a SurrealModel conformance, and a Fields namespace for type-safe predicates.
@SurrealModel("article")
struct Article: Codable, Sendable {
let id: String?
let title: String
let published: Bool
let views: Int
}
// Generated Fields:
// Article.Fields.id → SurrealField<Article, String?>
// Article.Fields.title → SurrealField<Article, String>
// Article.Fields.published → SurrealField<Article, Bool>
// Article.Fields.views → SurrealField<Article, Int>If you prefer not to use the macro, conform to SurrealModel manually:
struct Article: SurrealModel, Codable, Sendable {
static let surrealTable = "article"
let id: String?
let title: String
}Suitable for request/response workloads. Does not support live queries.
let client = try SurrealHTTPClient(endpoint: "http://localhost:8000")
try await client.connect()
defer { Task { await client.close() } }Required for live queries. Reconnects automatically by default.
let client = try SurrealWebSocketClient(
endpoint: "ws://localhost:8000",
websocketOptions: SurrealWebSocketOptions(
reconnectEnabled: true,
maxReconnectAttempts: 8,
reconnectBaseDelay: 0.5
)
)
try await client.connect()
defer { Task { await client.close() } }try await client.use(namespace: "myapp", database: "mydb")let tokens = try await client.signin(.root(username: "root", password: "root"))let tokens = try await client.signin(.namespace(
namespace: "myapp",
username: "ns_user",
password: "secret"
))let tokens = try await client.signin(.database(
namespace: "myapp",
database: "mydb",
username: "db_user",
password: "secret"
))let tokens = try await client.signin(.accessVariables(
namespace: "myapp",
database: "mydb",
access: "account",
variables: ["email": .string("user@example.com"), "pass": .string("secret")]
))let tokens = try await client.signin(.accessBearer(
namespace: "myapp",
database: "mydb",
access: "account",
key: "bearer-token-value"
))let tokens = try await client.signup(.accessRecord(
namespace: "myapp",
database: "mydb",
access: "account",
variables: ["email": .string("new@example.com"), "pass": .string("secret")]
))try await client.authenticate(tokens.access)
// Or start with a pre-existing token
let client = try SurrealHTTPClient(
endpoint: "http://localhost:8000",
session: SessionContext(
namespace: "myapp",
database: "mydb",
accessToken: "existing-jwt"
)
)try await client.invalidate()// All records
let people = try await client.select(Person.self)
// With a predicate, limit, and offset
let adults = try await client.select(
Person.self,
where: Person.Fields.age >= 18,
limit: 20,
start: 0
)let id = SurrealRecordID(table: "person", id: .string("ada"))
let person: Person? = try await client.select(recordID: id, as: Person.self)// Auto-generated ID
let created: [Person] = try await client.create(Person(id: nil, name: "Ada", age: 30))
// Specific record ID
let id = SurrealRecordID(table: "person", id: .string("ada"))
let record: Person? = try await client.create(
recordID: id,
content: Person(id: nil, name: "Ada", age: 30)
)// Update all matching records
let updated = try await client.update(
Person.self,
content: Person(id: nil, name: "Ada", age: 31),
where: Person.Fields.name == "Ada"
)
// Update a specific record
let id = SurrealRecordID(table: "person", id: .string("ada"))
let record: Person? = try await client.update(
recordID: id,
content: Person(id: nil, name: "Ada", age: 31)
)let upserted = try await client.upsert(
Person.self,
content: Person(id: nil, name: "Ada", age: 31),
where: Person.Fields.name == "Ada"
)// Delete matching records
let deleted = try await client.delete(Person.self, where: Person.Fields.age < 18)
// Delete a specific record
let id = SurrealRecordID(table: "person", id: .string("ada"))
let record: Person? = try await client.delete(recordID: id, as: Person.self)Fields properties support Swift comparison operators that produce type-safe SurrealPredicate values.
// Equality
Person.Fields.name == "Ada"
Person.Fields.name != "Bob"
// Comparisons
Person.Fields.age > 18
Person.Fields.age >= 21
Person.Fields.age < 65
Person.Fields.age <= 60
// Combining
let predicate = Person.Fields.age >= 18 && Person.Fields.published == true
let either = Person.Fields.age < 18 || Person.Fields.name == "Admin"
let negated = !(Person.Fields.published == false)
// Raw string predicate
let raw = SurrealPredicate(raw: "age > 18 AND name != 'Bot'")The SDK ships expression macros that resolve to SurrealDSL calls at compile time. Use them anywhere you would build a SurrealQuery by hand.
import SurrealDB
let selectQuery = #select(Person.self, where: Person.Fields.age > 18, limit: 10)
let createQuery = #create(Person.self)
let updateQuery = #update(Person.self, where: Person.Fields.name == "Ada")
let upsertQuery = #upsert(Person.self)
let deleteQuery = #delete(Person.self, where: Person.Fields.age < 18)
let liveQuery = #live(Person.self)
let people = try await client.query(selectQuery)SurrealDSL is the programmatic alternative to the macros, useful when you need to build queries at runtime.
let query = SurrealDSL.select(
Person.self,
where: Person.Fields.age >= 21,
limit: 50,
start: 0
)
let people = try await client.query(query)
let createQuery = SurrealDSL.create(
Person.self,
contentBinding: "content",
bindings: ["content": try .fromEncodable(newPerson)]
)
let created = try await client.query(createQuery)Live queries require SurrealWebSocketClient and return an AsyncStream<LiveEvent<T>>.
let client = try SurrealWebSocketClient(endpoint: "ws://localhost:8000")
try await client.connect()
_ = try await client.signin(.root(username: "root", password: "root"))
try await client.use(namespace: "myapp", database: "mydb")
let stream = try await client.live(SurrealDSL.live(Person.self))
for await event in stream {
switch event.action {
case .create:
print("Created:", event.decoded as Any)
case .update:
print("Updated:", event.decoded as Any)
case .delete:
print("Deleted record:", event.recordID)
case .killed:
print("Live query was killed")
}
}Cancel the stream by killing the live query:
// Capture the queryID from the first event, then:
try await client.kill(liveQueryID: event.queryID)Run arbitrary SurrealQL with bound parameters:
let results: [RPCQueryResult] = try await client.queryRaw(
"SELECT * FROM person WHERE age > $minAge LIMIT $limit;",
bindings: [
"minAge": .int(18),
"limit": .int(50)
]
)
for row in results {
if row.status == .ok {
print(row.result) // SurrealValue
}
}SurrealValue is the SDK's universal value type for working with raw SurrealDB data.
// Constructing values
let v: SurrealValue = .string("hello")
let v: SurrealValue = .int(42)
let v: SurrealValue = .double(3.14)
let v: SurrealValue = .bool(true)
let v: SurrealValue = .null
let v: SurrealValue = .array([.string("a"), .int(1)])
let v: SurrealValue = .object(["name": .string("Ada"), "age": .int(30)])
let v: SurrealValue = .uuid(UUID())
let v: SurrealValue = .datetime(Date())
let v: SurrealValue = .recordID(SurrealRecordID(table: "person", id: .string("ada")))
// Convert any Encodable to SurrealValue
let value = try SurrealValue.fromEncodable(myStruct)
// Decode a SurrealValue back to a Swift type
let person = try value.decode(Person.self)Applies to both HTTP and WebSocket clients:
SurrealClientOptions(
requestTimeout: 20, // seconds, default 20
pingInterval: 30 // seconds, default 30
)SurrealWebSocketOptions(
reconnectEnabled: true, // default true
maxReconnectAttempts: 8, // default 8
reconnectBaseDelay: 0.5 // seconds, default 0.5
)Pre-populate namespace, database, and access token at init time:
SessionContext(
namespace: "myapp",
database: "mydb",
accessToken: nil,
variables: [:]
)Run unit tests:
swift testRun integration tests (requires a running SurrealDB instance):
SURREALDB_RUN_INTEGRATION=1 swift testIntegration environment variables:
| Variable | Default |
|---|---|
SURREALDB_WS_ENDPOINT |
ws://127.0.0.1:8000 |
SURREALDB_HTTP_ENDPOINT |
http://127.0.0.1:8000 |
SURREALDB_ROOT_USER |
root |
SURREALDB_ROOT_PASS |
root |
SURREALDB_SKIP_SIGNIN |
(unset) |
Apache 2.0 - see LICENSE.