I'm excited to announce swift-kurrentdb 2.0.0 — a complete redesign of the Swift client for KurrentDB (formerly EventStoreDB).
If you're building Event Sourcing systems with Server-Side Swift, this release is for you.
Background
KurrentDB is a purpose-built event store for Event Sourcing. It stores events as an append-only log, supports projections, persistent subscriptions, and cluster deployments with leader election.
swift-kurrentdb is the community Swift client for it, built on top of grpc-swift 2.x and Swift Concurrency.
Version 1.x worked, but it wasn't designed with Swift 6's strict concurrency model in mind. Rather than patch it, 2.0 is a ground-up redesign that feels native to modern Swift.
What changed in 2.0
Target-based hierarchical API
The biggest change is how you interact with the client. The old flat-method style is replaced by a composable, target-based design:
// 1.x — flat methods on the client
try await client.appendToStream("orders", events: [event]) {
$0.revision(expected: .any)
}
// 2.x — select a target, then call the operation
try await client.streams(of: .specified("orders"))
.append(events: [event]) {
$0.expectedRevision = .any
}
The same pattern applies everywhere:
// Streams
client.streams(of: .specified("orders"))
client.streams(of: .all)
// Projections
client.projections(name: "order-count")
client.projections(system: .byCategory)
// Persistent Subscriptions
client.persistentSubscriptions(stream: "orders", group: "workers")
This makes the API more discoverable, consistent, and easier to extend.
Swift 6 strict concurrency — zero data races
KurrentDBClient is an actor. All public operations are async. The entire codebase compiles cleanly under -strict-concurrency=complete — no @unchecked Sendable shortcuts, no suppression pragmas.
Typed throws
Every operation throws KurrentError — not any Error. You always know exactly what can go wrong:
do {
try await client.streams(of: .specified("orders"))
.append(events: [event]) {
$0.expectedRevision = .streamExists
}
} catch KurrentError.wrongExpectedVersion {
// optimistic concurrency conflict — handle gracefully
} catch KurrentError.streamNotFound {
// stream doesn't exist yet
}
No more as? SomeError casting in catch blocks.
Cluster resilience
2.0 adds automatic node re-discovery when a cluster node fails. The client:
- Detects the failure via gRPC status codes
- Invalidates the cached node
- Re-runs gossip-based discovery to find a healthy node
- Retries the operation transparently
Retry policy and node cache TTL are now fully configurable:
let settings = ClientSettings.localhost()
.retryPolicy(.init(maxAttempts: 5, interval: .seconds(2)))
.nodeCacheTTL(.seconds(30))
Subscription dropped events
Persistent subscriptions now surface a subscriptionDropped error with lastRevision and lastPosition — the exact checkpoint to resume from after an unexpected disconnect:
do {
for try await result in subscription.events {
try await subscription.ack(readEvents: result.event)
}
} catch KurrentError.subscriptionDropped(let reason, let lastRevision, _) {
// resume from lastRevision on reconnect
}
Migrating from 1.x
I know breaking changes are painful. That's why 2.0 ships a KurrentDB_V1 compatibility library in the same package.
Switch your dependency target:
// Package.swift
.product(name: "KurrentDB_V1", package: "swift-kurrentdb")
Change your import:
import KurrentDB_V1 // was: import KurrentDB
That's it. Your existing 1.x code compiles and runs unchanged. All 1.x methods are marked @deprecated, so Xcode guides you through the migration at your own pace — no big-bang rewrites required.
Quality & compatibility
| Tests | 174 across 11 suites |
| Coverage | 75% line coverage |
| Test environment | Live 3-node TLS KurrentDB cluster |
| Server support | KurrentDB 25.1, 26.0 · EventStoreDB 24.x |
| Swift versions | 6.0, 6.1, 6.2 |
| Platforms | macOS 15+ · iOS 18+ · Linux |
Test suites cover streams (append, read, subscribe, optimistic concurrency), projections, persistent subscriptions (ACK/NACK, park, replay), users, operations, gossip, and monitoring — all running against a real cluster, not mocks.
Get started
// Package.swift
.package(url: "<https://github.com/gradyzhuo/swift-kurrentdb.git>", from: "2.0.0")
import KurrentDB
let client = KurrentDBClient(
settings: .localhost()
.authenticated(.credentials(username: "admin", password: "changeit"))
)
// Append an event
let event = EventData(eventType: "OrderPlaced", model: Order(id: "123", total: 99.99))
try await client.streams(of: .specified("orders")).append(events: [event])
// Read events
let stream = try await client.streams(of: .specified("orders")).read {
$0.startFrom(revision: .start).limit(10)
}
for try await response in stream {
print(try response.event?.record.eventType ?? "checkpoint")
}
Links
Feedback, issues, and PRs are very welcome. Event Sourcing in the Swift ecosystem is still young — let's build it together.
Top comments (0)