Skip to content

Add Kotlin Multiplatform SDK (JVM + iOS)#4471

Open
AndroidPoet wants to merge 5 commits intoclockworklabs:masterfrom
AndroidPoet:feat/kotlin-sdk
Open

Add Kotlin Multiplatform SDK (JVM + iOS)#4471
AndroidPoet wants to merge 5 commits intoclockworklabs:masterfrom
AndroidPoet:feat/kotlin-sdk

Conversation

@AndroidPoet
Copy link

@AndroidPoet AndroidPoet commented Feb 26, 2026

What

Kotlin Multiplatform client SDK for SpacetimeDB, targeting JVM and iOS (arm64, simulator-arm64, x64). This brings first-class Kotlin/Java/Swift (via KMP interop) support to the SpacetimeDB ecosystem.

Why

There's currently no way to build native Android/JVM or iOS clients without going through the C# or Rust SDKs. The Kotlin SDK fills this gap and opens up the entire JVM and Apple platform ecosystem — Android apps, backend services, iOS apps, and any Kotlin target.

Architecture

  • Wire protocol: Full v2.bsatn.spacetimedb support with BSATN binary serialization
  • Transport: Ktor 3.0.3 WebSocket client (OkHttp engine on JVM, Darwin engine on iOS)
  • Concurrency: kotlinx-coroutines with Mutex-serialized message handling, atomicfu for transport flags
  • Compression: Configurable (None/Gzip/Brotli) via withCompression(). Gzip on both platforms via expect/actual, Brotli on JVM via org.brotli:dec
  • Cache: Client-side row cache with ref-counted rows for overlapping subscription support

Features

  • Connect/disconnect with callbacks
  • SQL subscriptions (subscribe, unsubscribe, subscribeToAllTables)
  • Reducer invocation with result callbacks
  • One-off queries (both suspend and callback variants)
  • Automatic reconnection with configurable exponential backoff (ReconnectPolicy)
  • Ping/pong keep-alive (30s idle timeout, matching the Rust SDK pattern)
  • Configurable server message compression (None, Gzip, Brotli)
  • Builder pattern for connection setup (DbConnectionBuilder)

File layout

sdks/kotlin/
├── build.gradle.kts / settings.gradle.kts / gradle.properties
├── README.md / DEVELOP.md
├── src/
│   ├── commonMain/          # Shared code (all targets)
│   │   └── com/clockworklabs/spacetimedb/
│   │       ├── SpacetimeDBClient.kt      # DbConnection + builder + CompressionMode
│   │       ├── Identity.kt               # Identity, ConnectionId, Address, Timestamp
│   │       ├── ClientCache.kt            # Row cache + TableCache
│   │       ├── TableHandle.kt            # Per-table insert/delete/update callbacks
│   │       ├── SubscriptionHandle.kt     # Subscription lifecycle
│   │       ├── SubscriptionBuilder.kt    # Fluent subscription API
│   │       ├── ReconnectPolicy.kt        # Exponential backoff config
│   │       ├── Compression.kt            # expect declarations
│   │       ├── bsatn/                    # Reader, Writer, RowList
│   │       ├── protocol/                 # ServerMessage, ClientMessage, ProtocolTypes
│   │       └── websocket/                # WebSocketTransport
│   ├── jvmMain/             # JVM decompression (GZIPInputStream + BrotliInputStream)
│   ├── iosMain/             # iOS decompression (platform.zlib)
│   ├── commonTest/          # Shared tests
│   └── jvmTest/             # JVM-only tests (compression, benchmarks, live integration)

Test coverage

57+ tests across 11 test files, all passing:

Category Tests What it covers
BSATN 14 Reader/Writer round-trips for all primitives
Protocol 9 ServerMessage + ClientMessage encode/decode
Cache 10 Insert, delete, ref counting, transactions
OneOffQuery 3 Ok + Err decode variants
ReconnectPolicy 8 Backoff math, parameter validation
Edge cases 32 Boundary values, callback re-entrance, subscription states, invalid tags
Compression 4 Gzip round-trip, empty/large payloads
Performance 15 BSATN throughput, cache ops, message decode, gzip decompression
Live integration 6 Real server: connect, subscribe, reducer calls, one-off queries
Live edge cases 15 Real server: invalid SQL, bad modules, bad reducers, token reuse
TPS benchmark 1 Keynote-2 fund transfer benchmark (10 connections, 16k pipeline depth)

Performance

Micro-benchmarks (JVM, Apple M-series)

Operation Throughput
BSATN read ~3M rows/sec
BSATN write ~3M rows/sec
CallReducer encode ~5M msg/sec
InitialConnection decode ~3.5M msg/sec
Cache insert ~7M rows/sec
Live reducer round-trip ~10ms avg

Keynote-2 TPS benchmark

Tested against the same benchmark methodology as templates/keynote-2/spacetimedb-rust-client: 10 WebSocket connections, 16384 max in-flight reducers, Zipf-distributed account selection (alpha=0.5, 100k accounts), batched pipelining with 5s warmup + 5s measurement.

Both clients running with compression=None against SpacetimeDB 2.0.1 on localhost (Apple M-series):

Run Rust client (v1 protocol) Kotlin SDK (v2 protocol)
1 70,436 TPS 80,687 TPS
2 73,050 TPS 91,188 TPS
3 72,943 TPS 92,905 TPS
Avg 72,143 TPS 88,260 TPS

The Kotlin SDK achieves ~22% higher throughput on Apple Silicon. On x86 Linux the gap is expected to be narrower — both clients are firmly in the same performance tier.

Note: The two clients use different protocol versions (Rust: v1.bsatn, Kotlin: v2.bsatn) and different concurrency models (Rust: tokio threads, Kotlin: coroutines + Ktor OkHttp). This is not a language benchmark — it's a validation that the Kotlin SDK's architecture can fully saturate the server.

Bug fixes included

  • Race condition in callback registration: callReducer() and subscribe() now register callbacks synchronously before sending the message, preventing a race where the server response could arrive before the callback was registered
  • Orphaned reducer callbacks on disconnect: failPendingOperations() now clears the reducer callback map alongside one-off query deferreds
  • Callback re-entrance safety: TableHandle.fireInsert/fireDelete/fireUpdate now snapshot the callback map before iteration, preventing ConcurrentModificationException if a callback registers or removes other callbacks

Roadmap

Phase 1 — Code Generation & Typed Access

  • Typed table classes (generated data classes with BSATN serialization)
  • Typed reducer calls (conn.reducers.addPlayer("Alice"))
  • Typed event callbacks (table.onInsert { player: Player -> })
  • Requires a Kotlin backend in crates/codegen/

Phase 2 — Event System & Procedure Support

  • Rich event context (ReducerEventContext, SubscriptionEventContext) matching C#/Rust SDKs
  • CallProcedure support for non-transactional server-side functions

Phase 3 — Observability

  • Structured logging (SLF4J on JVM, OSLog on iOS)
  • Connection metrics (latency tracking, message counts, byte throughput)

Phase 4 — Framework Integrations

  • Jetpack Compose (rememberSpacetimeDB(), collectAsState() for tables)
  • Kotlin Flow adapters for reactive table subscriptions
  • SwiftUI wrappers via KMP interop

Phase 5 — Platform Expansion

  • Dedicated Android target with lifecycle integration and sample app
  • Maven Central publish with automated release pipeline
  • Light mode and confirmed reads connection options

Feature Parity

Feature Kotlin C# TypeScript Rust
BSATN Protocol done done done done
WebSocket Transport done done done done
Reconnection done done done done
Compression done done done done
Keep-Alive done done done done
Row Cache done done done done
Subscriptions done done done done
One-off Queries done done done done
Reducer Calls done done done done
Procedure Calls planned done done done
Code Generation planned done done done
Logging planned done done done
Metrics planned done done done
Framework Integration planned done (Unity) done (React/Vue)

Dependencies

io.ktor:ktor-client-core:3.0.3
io.ktor:ktor-client-websockets:3.0.3
io.ktor:ktor-client-okhttp:3.0.3 (JVM)
io.ktor:ktor-client-darwin:3.0.3 (iOS)
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
org.jetbrains.kotlinx:atomicfu:0.23.2
org.brotli:dec:0.1.2 (JVM)

Build

./gradlew jvmTest                     # run all JVM tests
./gradlew compileKotlinIosArm64       # verify iOS compilation
SPACETIMEDB_TEST=1 ./gradlew jvmTest  # run with live integration tests

Kotlin/Multiplatform client SDK targeting JVM and iOS (arm64, simulator-arm64, x64)
with full v2.bsatn.spacetimedb protocol support.

Includes:
- BSATN binary serialization/deserialization
- WebSocket transport with Gzip/Brotli decompression
- Client-side row cache with ref-counted rows
- SQL subscriptions and one-off queries
- Reducer invocation with result callbacks
- Automatic reconnection with exponential backoff
- Ping/pong keep-alive (30s idle timeout)
- Comprehensive test suite (47 tests across 6 test files)
- Fix race in subscribe()/callReducer() where map registration happened
  in a launched coroutine but the message was sent immediately, allowing
  the server to respond before the callback was registered
- Fix orphaned reducer callbacks on disconnect (failPendingOperations
  now clears reducerCallbacks)
- Fix potential ConcurrentModificationException in TableHandle fire
  methods by iterating a snapshot of callback values
- Add performance benchmarks, live integration tests, and edge case
  coverage (57 tests total, 0 failures)
@CLAassistant
Copy link

CLAassistant commented Feb 26, 2026

CLA assistant check
All committers have signed the CLA.

Add CompressionMode enum (None/Gzip/Brotli) to allow callers to
control server-to-client compression negotiation. Previously hardcoded
to Gzip; now defaults to Gzip but can be overridden via
DbConnectionBuilder.withCompression().

Add Keynote2BenchmarkTest that replicates the reference Rust benchmark
client from templates/keynote-2: 10 connections, 16384 max in-flight
reducers, Zipf-distributed account selection (alpha=0.5, 100k accounts),
batched pipelining with 5s warmup + 5s measurement.

Set test JVM heap to 1g to support the 10M pre-computed transfer pairs.
@bfops
Copy link
Collaborator

bfops commented Feb 27, 2026

Thank you for opening this! We've been thinking about Kotlin support so this might give us a great head start :) We'll work on getting a reviewer for it - at the moment we're still pretty busy with things related to our recent 2.0 launch.

Document the 5-phase roadmap covering code generation, event system,
observability, framework integrations, and platform expansion. Include
feature parity comparison with C#, TypeScript, and Rust SDKs, plus
keynote-2 benchmark summary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants