Skip to content

Repository files navigation

Bird Realtime for Kotlin

The official Bird Realtime client for Kotlin and the JVM: subscribe to channels and receive events in real time over a WebSocket.

Looking for the server side (sending messages, managing resources, verifying webhooks, publishing Realtime events)? Those live in the Bird API; see the API reference.

A plain JVM library, so it works in an Android app and on a server alike. Callbacks land on the Android main looper when there is one, and run inline otherwise.

Install

dependencies {
    implementation("com.messagebird:bird-realtime:0.1.0")
}

Android also needs the internet permission, which a library no longer contributes:

<uses-permission android:name="android.permission.INTERNET" />

Quickstart

Browsable example: examples/quickstart-realtime.kt

import com.messagebird.realtime.BirdRealtime
import com.messagebird.realtime.BirdRealtimeOptions

val bird = BirdRealtime(
    BirdRealtimeOptions(
        appKey = "your-app-key",
        region = "us1", // us1 | eu1, picks the edge automatically
    )
)

val orders = bird.subscribe("orders")
orders.bind("order-updated") { data ->
    println("order changed: $data")
}

bird.onConnectionStateChange { previous, current ->
    println("connection: $previous -> $current")
}
bird.onError { error ->
    println("realtime error: ${error.message}")
}

The constructor opens the connection; connect() is only for reopening after disconnect().

Event payloads arrive as kotlinx.serialization.json.JsonElement, so read them with the serialization API rather than casting.

Private and presence channels

Subscriptions to private- / presence- channels are signed by your backend, which holds the app secret. Point the client at your auth endpoint: it POSTs {"connection_id", "channel_name"} and expects {"auth", "member_data"?} back.

val bird = BirdRealtime(
    BirdRealtimeOptions(
        appKey = "your-app-key",
        region = "us1",
        authEndpoint = "https://your-backend.example.com/bird/auth",
        authHeaders = mapOf("authorization" to "Bearer <session token>"),
    )
)

val room = bird.subscribe("presence-room-42")
if (room is PresenceChannel) {
    room.bind(BirdProtocol.Event.SUBSCRIPTION_SUCCEEDED) {
        println("me: ${room.myId}, members: ${room.members.keys}")
    }
    room.bind(BirdProtocol.Event.MEMBER_ADDED) { member -> println("joined: $member") }
}

Supply a custom authorizer instead to sign through your own networking stack.

Server-side subscription rejections (bad signature, capacity) arrive on the connection rather than the channel, because the wire carries no channel attribution, so observe them with onError. An authorizer failure does emit bird:subscription_error on the channel.

Signing in a member

signin() tells the edge who this connection belongs to, which is what lets the events API address a member and the disconnect API terminate them. It is also what satisfies an app configured to require authorized connections.

val bird = BirdRealtime(
    BirdRealtimeOptions(
        appKey = "your-app-key",
        region = "us1",
        memberAuthEndpoint = "https://your-backend.example.com/bird/auth/member",
    )
)

val me = bird.signin() // suspending; call once, survives reconnects
println("signed in as ${me.memberId}")

bird.onSigninError { error ->
    println("re-signin failed: ${error.message}") // still connected, no identity
}

Your endpoint receives {"connection_id"} and returns {"auth", "member_data"}, where member_data is the JSON string (carrying member_id) that your backend signed.

The identity lives on the connection, so it is dropped when the connection drops and re-established on the next one. The re-signin has nothing to throw to, which is what onSigninError is for; it is separate from onError so a failing member endpoint cannot disturb channel subscriptions.

Events addressed to a member

Your server can send an event to a member rather than to a channel, reaching every connection that member holds. Once signin() succeeds the client subscribes the member's reserved channel automatically; bind on bird.member:

bird.signin()

bird.member.bind("order.shipped") { data ->
    println("your order moved: $data")
}

Client events

On a subscribed private/presence channel, with the app's client-events setting enabled:

room.trigger("client-typing", buildJsonObject { put("on", true) })

Behaviour

  • Reconnection is automatic, with full-jitter exponential backoff (1s base, 30s cap). Close codes 4000-4099 are refusals and terminal (no retry; the code is surfaced through onError); 4200-4299 retry immediately; everything else backs off.
  • Liveness: the client pings after the activity timeout (server-supplied, default 120s) and reconnects if no pong arrives within 30s.
  • Channels re-subscribe automatically on every reconnect, with fresh authorization; handlers survive the round-trip.
  • TLS always for non-loopback hosts. allowInsecure is honored only for localhost, 127.0.0.1 and [::1], so a copied config cannot silently downgrade real traffic.
  • Callbacks run on the Android main looper when one exists, and inline on a plain JVM.
  • Close the client when you are done with it. BirdRealtime is AutoCloseable: close() disconnects and releases the thread it confines its state to. disconnect() deliberately does not, since it leaves the client reusable by connect(), and the JVM has no destructor to fall back on, so a dropped client keeps its worker thread and its socket for the life of the process.
  • Errors all extend the sealed BirdRealtimeException, so one catch covers the SDK and a when over it is exhaustive. RealtimeException carries the server's close code where there is one; a failing authorization endpoint throws RealtimeAuthException with the endpoint and HTTP status instead, so an HTTP 403 never arrives in the field you check for close code 4009. A channel authorization failure reaches bind("bird:subscription_error") rather than being thrown, and carries the same endpoint and status as payload fields.

Channels and events

Name prefix Type Authorized
(none) public no
private- private yes
presence- presence yes

signin() adds a fourth authorized surface: it signs <connection_id>::member::<member_data> rather than a channel name, so a presence auth response can never be replayed as a signin.

Lifecycle events available to bind: bird:subscription_succeeded, bird:subscription_error, bird:connection_count, and on presence channels bird:member_added / bird:member_removed.

Not yet implemented

  • End-to-end encrypted channels (missing from every Bird Realtime client, not just this one)
  • connecting_in (the countdown to the next reconnect attempt)
  • Mobile lifecycle helpers: background/foreground socket handling, ConnectivityManager reachability

Development

gradle :realtime:build

Tests drive the client against a scripted in-memory transport, so they need no network. gradle :livedemo:run drives a live edge from the terminal.

License

MIT

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages