Skip to main content

WebSocket Protocol

JetStart uses WebSockets for all real-time communication between the Core server, Android clients, and the web emulator. Every connection is session-scoped and token-authenticated.

Connection Details

PropertyValue
Default port8766
Path/
SubprotocolsNone
Close code 4001Session mismatch — rescan QR code
Close code 4002Token mismatch — rescan QR code

Authentication

Immediately after the WebSocket connection is established, the client must send client:connect with the correct sessionId and token (both encoded in the QR code). If either value does not match the active session, the server closes the connection immediately with the appropriate close code. Any further messages from an unauthenticated client are ignored.

Base Message Shape

All messages share this base structure:

interface BaseMessage {
type: string; // Identifies the message
timestamp: number; // Unix ms
sessionId?: string; // Required on most messages after authentication
}

Client → Core Messages

Sent by the Android app or web emulator to the Core server.

client:connect

Opens a session. Must be the first message sent.

{
"type": "client:connect",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"token": "xyz789",
"deviceInfo": {
"id": "device-abc",
"model": "Pixel 7",
"platform": "android",
"architecture": "arm64",
"androidVersion": "14",
"apiLevel": 34
}
}

On success, the server responds with core:connected and triggers the initial build.

client:status

Reports the client's current state.

{
"type": "client:status",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"status": "connected",
"message": "Waiting for changes"
}

client:log

Forwards a device log entry to the Core server, which relays it to the Logs server and broadcasts it as core:log to dashboard clients.

{
"type": "client:log",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"log": {
"id": "log-001",
"timestamp": 1711900000000,
"level": "info",
"tag": "JetStart",
"message": "Activity resumed",
"source": "client"
}
}

client:heartbeat

Keep-alive ping sent every 30 seconds.

{
"type": "client:heartbeat",
"timestamp": 1711900000000
}

No response is expected.

client:disconnect

Sent before the client closes the connection gracefully.

{
"type": "client:disconnect",
"timestamp": 1711900000000,
"reason": "User backgrounded app"
}

client:click

UI interaction event from the web emulator (Android clients do not send this).

{
"type": "client:click",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"action": "onClick",
"elementType": "Button",
"elementText": "Submit"
}

Core → Client Messages

Sent by the Core server to all authenticated clients in a session.

core:connected

Authentication accepted. Sent immediately after a valid client:connect.

{
"type": "core:connected",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"projectName": "my-awesome-app"
}

After receiving this, the client can expect a core:build-start as the initial build is triggered.

core:build-start

A Gradle build has started.

{
"type": "core:build-start",
"timestamp": 1711900000000,
"sessionId": "a1b2c3"
}

core:build-status

Mid-build progress update.

{
"type": "core:build-status",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"status": "compiling_kotlin"
}

core:build-complete

Build succeeded. The APK is available for download.

{
"type": "core:build-complete",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"apkInfo": {
"path": "/path/to/app-debug.apk",
"size": 5242880,
"hash": "abc123",
"versionCode": 1,
"versionName": "1.0.0",
"minSdkVersion": 24,
"targetSdkVersion": 34,
"applicationId": "com.example.app"
},
"downloadUrl": "http://192.168.1.100:8765/download/app-debug.apk"
}

core:build-error

Build failed.

{
"type": "core:build-error",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"error": "Compilation failed",
"details": "Unresolved reference: MyClass at MainActivity.kt:42"
}

core:dex-reload

The primary hot reload message for Android devices. Carries base64-encoded DEX bytecode compiled from the changed Kotlin file. Android clients decode this and load the new classes via a custom ClassLoader — no reinstall required.

{
"type": "core:dex-reload",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"dexBase64": "ZGV4CjAzNQA...",
"classNames": [
"com.example.app.MainActivity",
"com.example.app.MainActivity$Override"
]
}

classNames lists every class patched in this reload. The $Override suffix denotes InstantRun-style companion classes that apply method-level patches.

core:js-update

Hot reload message for the web emulator. Carries a base64-encoded ES module compiled by kotlinc-js from the same changed file. Browser clients import it dynamically and re-render the Compose UI preview. Android clients ignore this message.

{
"type": "core:js-update",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"jsBase64": "aW1wb3J0IHt...",
"sourceFile": "MainActivity.kt",
"byteSize": 4096
}

core:reload

Instructs clients to perform a reload of a specific type. Used for explicit reload triggers.

{
"type": "core:reload",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"reloadType": "hot"
}

reloadType is either "hot" or "full".

core:ui-update

Sends a DSL (JSON) representation of the Compose UI tree. Used by the web emulator's DSLRenderer for static visual preview when a compiled JS module is not yet available.

{
"type": "core:ui-update",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"dslContent": "{\"type\":\"Column\",\"children\":[...]}",
"screens": ["MainActivity"],
"hash": "abc-123"
}

core:log

Broadcasts a device log entry to all dashboard/CLI clients subscribed to the session. Not sent to the originating Android client.

{
"type": "core:log",
"timestamp": 1711900000000,
"sessionId": "a1b2c3",
"log": {
"id": "log-001",
"timestamp": 1711900000000,
"level": "info",
"tag": "JetStart",
"message": "DEX loaded — 2 classes patched",
"source": "client"
}
}

core:disconnect

Server is shutting down.

{
"type": "core:disconnect",
"timestamp": 1711900000000,
"reason": "Server stopped"
}

Message Flow Examples

Initial Connection and First Build

Client                          Core Server
│ │
│──── WebSocket open ─────────────►│
│ │
│──── client:connect ─────────────►│ (sessionId + token)
│ │ Validates credentials
│Γùä─── core:connected ──────────────│ (projectName)
│ │
│Γùä─── core:build-start ────────────│ Initial build begins
│Γùä─── core:build-status ───────────│ Progress updates
│Γùä─── core:build-complete ─────────│ APK download URL
│ │
│ Client downloads + installs APK │

Hot Reload (File Save → Device Update)

Developer saves .kt file

│ Core Server
│ ├─ KotlinCompiler → .class files
│ ├─ OverrideGenerator → $Override classes
│ └─ DexGenerator → classes.dex

Core Server Android Client Web Emulator
│ │ │
│──── core:dex-reload ─────────────►│ │
│ (base64 DEX, classNames) │ │
│ │ ClassLoader │
│ │ loads DEX │
│ │ Recompose │
│ │ │
│──── core:js-update ───────────────────────────────►│
│ (base64 ES module) │ │
│ │ dynamic import()
│ │ Re-renders UI

Error Handling

If the server rejects a connection, it closes the WebSocket with a code and reason:

Close CodeReasonAction
4001Session mismatchRescan QR code — device was built against a different session
4002Token mismatchRescan QR code — session token does not match
1001Server shutdownReconnect when server restarts

Clients should implement automatic reconnect with exponential backoff (max 5 attempts, starting at 5 seconds).


Logs Server (Port 8767)

A separate WebSocket server on port 8767 handles log aggregation independently of the main protocol. See the @jetstart/logs package documentation for its own message protocol (subscribe, log, clear, stats).