A real time collaborative whiteboard built with Kotlin Multiplatform and Compose Multiplatform. Multiple clients draw on a shared canvas, see each other's live cursors, and stay in sync over WebSockets with server side persistence.
- Template Generated via Catylst KMP Starter (Android Studio Plugin)
- Real time collaborative drawing synced across all connected clients
- Live cursor presence so you can see where others are pointing
- Broadcasting of stroke events to every client over WebSockets
- Server side persistence so boards survive restarts and late joiners
- Configurable display name on join
- Shared toolbar for drawing controls
- Language: Kotlin 2.3.21
- UI: Compose Multiplatform 1.11.0-rc01, Material3 1.11.0-alpha06
- Networking: Ktor 3.4.3 (client and server, WebSockets)
- Dependency Injection: Koin 4.1.1
- Navigation: Navigation3 1.1.1
- Serialization: kotlinx.serialization 1.9.0
- Persistence: Exposed 0.56.0 with SQLite JDBC 3.47.1.0
- Concurrency: kotlinx.coroutines 1.10.2
- Build: Gradle with AGP 9.1.1, KSP 2.3.7
- Android
- iOS
- Desktop (JVM)
composeApp— shared Compose Multiplatform UI and client logicandroidApp— Android application entry pointserver— Ktor WebSocket server with persistenceshared— models and code shared between client and server
- JDK 17 or newer
- Android Studio or IntelliJ IDEA (latest stable)
- Xcode (only required for the iOS target)
- Clone the repository
- Open the project in Android Studio or IntelliJ IDEA
- Sync Gradle
./gradlew :server:run./gradlew :androidApp:assembleDebug./gradlew :composeApp:run./gradlew :composeApp:wasmJsBrowserDevelopmentRunOpen the iOS project in Xcode and run it on a simulator or device.
./gradlew :composeApp:wasmJsBrowserDevelopmentRunThe app runs in production with the server on Render and the web client on Vercel. All clients (Web, Desktop, iOS, Android) point at the deployed server over wss:// (see the platform entry points, e.g. composeApp/src/wasmJsMain/kotlin/io/jadu/wangdu/main.kt).
The Dockerfile at the repo root builds and runs the Ktor server as a Docker web service.
- Language: Docker
- Root Directory: leave blank (the build context must be the repo root, since the server depends on
:sharedand the Gradle wrapper) - Port: the app reads Render's injected
PORTenv var automatically (Application.kt); no configuration needed - Persistence (optional): attach a Render Disk mounted at
/datato keep the SQLite database (DB_PATHdefaults to/data/whiteboard.db). Without a disk, board data resets on each deploy since the container filesystem is ephemeral.
A trimmed deploy/settings.gradle.kts is used inside the container so only :server and :shared are built (the app modules are skipped). Render redeploys automatically on every push to main.
.github/workflows/deploy-web.yml builds the Kotlin/Wasm distribution and deploys it to Vercel on every push to main.
Required GitHub repository secrets:
| Secret | Where to find it |
|---|---|
VERCEL_TOKEN |
Vercel → Settings → Tokens → Create Token |
VERCEL_ORG_ID |
Your Vercel Team ID (Settings → General), or orgId in .vercel/project.json after npx vercel link |
VERCEL_PROJECT_ID |
projectId in .vercel/project.json after npx vercel link |
To deploy the web app manually instead:
./gradlew :composeApp:wasmJsBrowserDistribution
cd composeApp/build/dist/wasmJs/productionExecutable
npx vercel --prodThis project is built for learning. The choices below keep it simple to run on a single machine, but they are not safe or scalable for real users. Treat this as a map of what to change before shipping.
-
Replace SQLite with Postgres. SQLite is a single file on disk. It locks on concurrent writes, so many users drawing at once contend for the same file. Postgres handles concurrent writes safely and provides proper backups and replication.
-
Switch from ws:// to wss://. Plain WebSocket traffic is unencrypted, so anyone on the network path can read or tamper with it. Production must use wss:// (WebSocket Secure), which needs an SSL certificate on the server, usually terminated by a reverse proxy such as Nginx or Caddy.
-
Add authentication. Any client can currently connect and claim any display name. Users should sign in and the server should verify their identity before accepting events, so names and actions can be trusted.
-
Add room support. Every connected user shares one global board today. A real app gives each board its own room with a separate WebSocket route, session registry, and database partition, so boards stay isolated.
-
Add rate limiting. A malicious or buggy client could send thousands of stroke events per second and overload the server. Limiting the event rate per session protects the server and keeps one client from degrading the experience for everyone.
-
Handle server restarts gracefully. If the server restarts, clients currently lose their connection with no explanation. Clients should detect the disconnect and automatically reconnect with exponential backoff so a restart is a brief blip rather than a dead app.
-
Add stroke count limits. The database currently stores strokes forever, so a board grows without bound. Production boards should cap the number of strokes or expire old ones by age to keep storage and load times under control.
Beyond production hardening, here are ideas to build on top of this project:
-
Undo and redo. Track a per user history of stroke operations so a client can revert its own actions without affecting others.
-
Multiple boards and a lobby. Add a home screen listing available boards, creating new ones, and joining by code, backed by the room support described above.
-
Richer drawing tools. Add shapes, text, arrows, an eraser, fill, and adjustable stroke width and opacity beyond freehand drawing.
-
Export and share. Let users export a board as an image or vector file, or share a read only snapshot link.
-
Infinite and zoomable canvas. Support panning and zooming on a large virtual canvas with viewport based rendering for performance.
-
Presence and chat. Show an avatar list of who is online and add a lightweight text chat alongside the board.
-
Conflict free sync with CRDTs. Move from broadcast based sync to a CRDT model so offline edits merge cleanly when clients reconnect.
-
Media on the canvas. Allow dropping images or sticky notes onto the board, uploaded to object storage and referenced by URL.
-
Web target. Extend the Compose Multiplatform app to run in the browser via Kotlin/Wasm so the whiteboard works without an install.
-
Playback and versioning. Persist an event log so a board can be replayed over time or restored to an earlier state.
Contributions are welcome. Please read the Contributing Guidelines to get started, and note that this project follows a Code of Conduct.
This project is licensed under the MIT License. See the LICENSE file for details.
Build with Love ❤️ by Jadu