Sockudo
Realtime Clients

Kotlin

Use sockudo-kotlin on Android and JVM with auth, recovery, rewind, filters, deltas, history proxies, and push registration.

sockudo-kotlin is the official Android and JVM realtime SDK. It uses OkHttp for WebSockets and exposes the same channel model as other Sockudo clients.

The current package targets JVM 23 and uses Protocol V2 by default. Set protocolVersion = 1 only when strict Pusher Protocol V1 compatibility is required.

Install

Install the published package from Maven Central:

dependencies {
    implementation("io.sockudo:sockudo-kotlin:2.1.0")
}

For Maven projects:

<dependency>
  <groupId>io.sockudo</groupId>
  <artifactId>sockudo-kotlin</artifactId>
  <version>2.1.0</version>
</dependency>

Connect

import io.sockudo.client.SockudoClient
import io.sockudo.client.SockudoOptions
import io.sockudo.client.SockudoTransport

val client =
    SockudoClient(
        "app-key",
        SockudoOptions(
            cluster = "local",
            forceTls = false,
            enabledTransports = listOf(SockudoTransport.ws),
            wsHost = "127.0.0.1",
            wsPort = 6001,
            wssPort = 6001,
            protocolVersion = 2,
            connectionRecovery = true,
        ),
    )

val channel = client.subscribe("public-updates")
channel.bind("price-updated") { data, _ ->
    println(data)
}

client.connect()

For production, point wsHost at the public load balancer or ingress, set forceTls = true, and use the secure WebSocket port. Keep one long-lived client per application process or signed-in mobile session.

Lifecycle and cleanup

Connection and channel events use the same binding model as application events:

val stateToken =
    client.bind("state_change") { change, _ ->
        println("connection: $change")
    }

val eventToken =
    channel.bind("price-updated") { data, _ ->
        println(data)
    }

channel.unbind("price-updated", eventToken)
client.unbind("state_change", stateToken)
client.unsubscribe("public-updates")
client.disconnect()

Wait for the channel subscription-success event before treating channel state as live. Unsubscribe and unbind when an Android lifecycle owner no longer needs updates; disconnect on explicit sign-out. A temporary network loss should be left to the SDK's reconnect path.

Auth

import io.sockudo.client.*

val client =
    SockudoClient(
        "app-key",
        SockudoOptions(
            cluster = "local",
            forceTls = false,
            wsHost = "127.0.0.1",
            wsPort = 6001,
            channelAuthorization =
                ChannelAuthorizationOptions(
                    endpoint = "https://api.example.com/sockudo/auth",
                ),
        ),
    )

Your auth endpoint must validate the application session and authorize the exact requested channel. For presence, derive user_id from the server-side identity. Never ship the app secret or an encryption master key in the APK.

Protocol V2 capability tokens can be supplied with a refresh provider:

val client =
    SockudoClient(
        "app-key",
        SockudoOptions(
            cluster = "local",
            protocolVersion = 2,
            authTokenProvider =
                ClientAuthTokenProvider { request ->
                    fetchCapabilityToken(
                        reason = request.reason,
                        socketId = request.socketId,
                    )
                },
        ),
    )

JWT expiry metadata enables proactive refresh. Opaque tokens refresh after the server reports expiration.

Presence

val presence = client.subscribe("presence-lobby") as PresenceChannel

presence.bind("sockudo:member_added") { member, _ -> println("joined: $member") }
presence.bind("sockudo:member_removed") { member, _ -> println("left: $member") }
presence.bind("sockudo:presence_update") { member, _ -> println("updated: $member") }

presence.update(mapOf("status" to "typing"))

The update method changes V2 presence member data without a leave/rejoin cycle.

Filters and deltas

val channel =
    client.subscribe(
        "price:btc",
        SubscriptionOptions(
            filter = Filter.eq("market", "spot"),
            events = listOf("price.updated"),
            expression = SubscriptionExpression.Source("data.price >= `100`"),
            delta = ChannelDeltaSettings(
                enabled = true,
                algorithm = DeltaAlgorithm.xdelta3,
            ),
        ),
    )

Recovery and rewind

val channel =
    client.subscribe(
        "market:BTC",
        SubscriptionOptions(rewind = SubscriptionRewind.Seconds(30)),
    )

channel.bind("message") { _, _ ->
    println(client.getRecoveryPosition("market:BTC"))
}

client.bind("sockudo:resume_success") { data, _ ->
    println(data)
}

Presence history proxy

val client =
    SockudoClient(
        "app-key",
        SockudoOptions(
            cluster = "local",
            forceTls = false,
            wsHost = "127.0.0.1",
            wsPort = 6001,
            presenceHistory =
                PresenceHistoryOptions(
                    endpoint = "https://api.example.com/sockudo/presence-history",
                ),
        ),
    )

val channel = client.subscribe("presence-lobby") as PresenceChannel
val page = channel.history(PresenceHistoryParams(limit = 50, direction = "newest_first"))
val snapshot = channel.snapshot(PresenceSnapshotParams(atSerial = 4))

History, snapshots, and versioned-message helpers call your trusted proxy. The proxy must authenticate the caller, authorize the channel, bound page sizes, and sign the upstream Sockudo request. Use channel history with untilAttach = true when building a gap-free late join.

Mutable messages

Apply V2 message actions in serial order. An update replaces local data, a delete becomes the latest visible version, and an append concatenates to a known string base. Configure VersionedMessagesOptions.endpoint for proxy-backed writes:

val chat = client.subscribe("chat:room-1")
val ack = chat.createMessage("chat.message", mapOf("text" to "hello"))
chat.appendMessage(ack.messageSerial, " world")
chat.updateMessage(ack.messageSerial, mapOf("text" to "edited"))
chat.deleteMessage(ack.messageSerial)

If an append arrives before its base, fetch the latest message through the proxy before applying subsequent actions.

Encrypted channels

private-encrypted-* subscriptions decrypt automatically when the auth response includes the derived channel shared secret:

val encrypted = client.subscribe("private-encrypted-documents")
encrypted.bind("doc-updated") { payload, _ -> println(payload) }

Encrypted channels still require TLS and normal channel authorization.

Android push registration

Use Firebase Messaging or the platform provider to obtain a token, then register through your backend.

class PushTokenService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        registerDeviceWithBackend(
            deviceId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID),
            platform = "fcm",
            providerToken = token,
        )
    }
}

The backend should call Sockudo push registration APIs with app credentials. The Android app should not hold Sockudo secrets.

Production checklist

  • Tie subscriptions to an Android lifecycle owner and avoid duplicate bindings after configuration changes.
  • Keep callbacks short; move blocking work off the WebSocket callback path.
  • Treat auth failures as permission failures instead of endlessly reconnecting.
  • On sockudo:resume_failed, reload authoritative application state.
  • Bound rewind and proxy history requests.
  • Refresh FCM tokens through the backend when onNewToken fires.
  • Test airplane-mode transitions, process recreation, capability-token expiry, and rolling Sockudo restarts.

On this page