
Self-hosted crash reporting capturing crashes, non-fatal errors, breadcrumbs and metadata; stores events in PostgreSQL, exposes a Compose-based dashboard, JWT role auth, multi-app support, single Docker deploy.
Open-source, self-hosted crash reporting for Kotlin Multiplatform apps.
CrashWatch captures unhandled exceptions and non-fatal errors from Android, iOS, Desktop, and Web targets, stores them in PostgreSQL, and surfaces them in a Compose Multiplatform dashboard — all running on your own infrastructure.
Hosted version: Don't want to self-host? crashwatch.dev offers a fully managed SaaS with a free trial.
docker compose commandlocalhost for local testing)curl -O https://raw.githubusercontent.com/sobuumedia/crashwatch/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/sobuumedia/crashwatch/main/.env.example
cp .env.example .envGenerate a JWT secret with openssl rand -hex 32 and paste the result — a dotenv
file does not expand $(...).
DB_PASS=a_strong_database_password
JWT_SECRET=paste_output_of_openssl_rand_hex_32
ADMIN_USERNAME=admin
ADMIN_PASSWORD=a_strong_admin_password
# Your public domain (dashboard + API are served same-origin via the proxy):
SITE_ADDRESS=crashwatch.example.com
ALLOWED_ORIGIN=https://crashwatch.example.com
ACME_EMAIL=admin@example.com
JWT_EXPIRY_HOURS=24
DEV_MODE=falsedocker compose -f docker-compose.prod.yml up -dThis starts PostgreSQL, the API server, the web dashboard, and a Caddy reverse proxy that terminates TLS and serves everything on one origin.
# Health check (through the proxy):
curl https://crashwatch.example.com/health # → {"status":"ok"}
# Log in:
curl https://crashwatch.example.com/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<ADMIN_PASSWORD>"}'
# → {"token":"...","role":"ADMIN"}Open https://crashwatch.example.com in a browser to reach the dashboard.
Local testing:
docker compose up --build(the non-prod file) serves the same stack over plain HTTP athttp://localhost(dashboard) with the API also exposed directly onhttp://localhost:8080.
| Variable | Required | Default | Description |
|---|---|---|---|
DB_PASS |
Yes | — | PostgreSQL password for the crashwatch user |
JWT_SECRET |
Yes | — | Secret for signing JWTs — use openssl rand -hex 32
|
ADMIN_USERNAME |
Yes | — | Username for the initial admin account |
ADMIN_PASSWORD |
Yes | — | Password for the initial admin account |
ALLOWED_ORIGIN |
Yes | — | CORS origin allowed to access the API. Set it to the same public origin as the dashboard, e.g. https://crashwatch.example.com
|
SITE_ADDRESS |
Prod | — | Public domain for the Caddy proxy; Caddy fetches a Let's Encrypt cert for it |
ACME_EMAIL |
Prod | — | Email for Let's Encrypt registration |
JWT_EXPIRY_HOURS |
No | 24 |
How long issued tokens remain valid |
DEV_MODE |
No | false |
Set true to allow any CORS origin (dev only) |
DB_URL |
No | set in compose | JDBC URL (overridden in docker-compose) |
DB_DRIVER |
No | set in compose | JDBC driver class |
DB_USER |
No | set in compose | Database username |
# Start a throwaway local Postgres (Flyway 11 no longer supports SQLite):
docker run --rm -d --name cw-dev-db -p 5432:5432 \
-e POSTGRES_DB=crashwatch -e POSTGRES_USER=crashwatch \
-e POSTGRES_PASSWORD=crashwatch postgres:16
./gradlew :server:installDist # build runnable distribution → server/build/install/server
./gradlew :server:run # run locally on http://localhost:9000 (admin/admin)
./gradlew :server:test # run server tests (embedded SQLite, no external database)
:server:runserves on port 9000; in Docker/production the server bindsPORT(default 8080). For a full local stack identical to production, usedocker compose up --buildinstead.
./gradlew :composeApp:run # Desktop
./gradlew :composeApp:assembleDebug # Android APK
./gradlew :composeApp:wasmJsBrowserDevelopmentRun # Web (Wasm)
./gradlew :composeApp:jsBrowserDevelopmentRun # Web (JS)Open iosApp/iosApp.xcodeproj in Xcode to build for iOS.
Builds and runs the whole stack (Postgres + API + web dashboard + proxy) over plain HTTP:
cp .env.example .env # fill in values
docker compose up --build
# dashboard → http://localhost · API → http://localhost:8080Full setup guides for all targets are in crashreporter/SDK_SETUP.md:
The dashboard ships three ways, all from the same Compose Multiplatform codebase:
SITE_ADDRESS. No separate install../gradlew :composeApp:run.On first launch of the desktop/mobile app, enter your server URL and admin credentials to log in. (The web dashboard is same-origin, so it just needs your admin credentials.)
CrashWatch has two interchangeable, API-compatible backends:
server/) — the default. Runs on the JVM with PostgreSQL; this is what
the Docker images and docker-compose*.yml use.php-backend/) — a drop-in alternative for cheap shared hosting (PHP +
MySQL, no JVM/Docker required). See php-backend/README.md.Both expose the same HTTP API, so the SDK and dashboard work against either.
See CONTRIBUTING.md for how to report bugs, suggest features, and submit pull requests.
MIT — see LICENSE.
Open-source, self-hosted crash reporting for Kotlin Multiplatform apps.
CrashWatch captures unhandled exceptions and non-fatal errors from Android, iOS, Desktop, and Web targets, stores them in PostgreSQL, and surfaces them in a Compose Multiplatform dashboard — all running on your own infrastructure.
Hosted version: Don't want to self-host? crashwatch.dev offers a fully managed SaaS with a free trial.
docker compose commandlocalhost for local testing)curl -O https://raw.githubusercontent.com/sobuumedia/crashwatch/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/sobuumedia/crashwatch/main/.env.example
cp .env.example .envGenerate a JWT secret with openssl rand -hex 32 and paste the result — a dotenv
file does not expand $(...).
DB_PASS=a_strong_database_password
JWT_SECRET=paste_output_of_openssl_rand_hex_32
ADMIN_USERNAME=admin
ADMIN_PASSWORD=a_strong_admin_password
# Your public domain (dashboard + API are served same-origin via the proxy):
SITE_ADDRESS=crashwatch.example.com
ALLOWED_ORIGIN=https://crashwatch.example.com
ACME_EMAIL=admin@example.com
JWT_EXPIRY_HOURS=24
DEV_MODE=falsedocker compose -f docker-compose.prod.yml up -dThis starts PostgreSQL, the API server, the web dashboard, and a Caddy reverse proxy that terminates TLS and serves everything on one origin.
# Health check (through the proxy):
curl https://crashwatch.example.com/health # → {"status":"ok"}
# Log in:
curl https://crashwatch.example.com/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<ADMIN_PASSWORD>"}'
# → {"token":"...","role":"ADMIN"}Open https://crashwatch.example.com in a browser to reach the dashboard.
Local testing:
docker compose up --build(the non-prod file) serves the same stack over plain HTTP athttp://localhost(dashboard) with the API also exposed directly onhttp://localhost:8080.
| Variable | Required | Default | Description |
|---|---|---|---|
DB_PASS |
Yes | — | PostgreSQL password for the crashwatch user |
JWT_SECRET |
Yes | — | Secret for signing JWTs — use openssl rand -hex 32
|
ADMIN_USERNAME |
Yes | — | Username for the initial admin account |
ADMIN_PASSWORD |
Yes | — | Password for the initial admin account |
ALLOWED_ORIGIN |
Yes | — | CORS origin allowed to access the API. Set it to the same public origin as the dashboard, e.g. https://crashwatch.example.com
|
SITE_ADDRESS |
Prod | — | Public domain for the Caddy proxy; Caddy fetches a Let's Encrypt cert for it |
ACME_EMAIL |
Prod | — | Email for Let's Encrypt registration |
JWT_EXPIRY_HOURS |
No | 24 |
How long issued tokens remain valid |
DEV_MODE |
No | false |
Set true to allow any CORS origin (dev only) |
DB_URL |
No | set in compose | JDBC URL (overridden in docker-compose) |
DB_DRIVER |
No | set in compose | JDBC driver class |
DB_USER |
No | set in compose | Database username |
# Start a throwaway local Postgres (Flyway 11 no longer supports SQLite):
docker run --rm -d --name cw-dev-db -p 5432:5432 \
-e POSTGRES_DB=crashwatch -e POSTGRES_USER=crashwatch \
-e POSTGRES_PASSWORD=crashwatch postgres:16
./gradlew :server:installDist # build runnable distribution → server/build/install/server
./gradlew :server:run # run locally on http://localhost:9000 (admin/admin)
./gradlew :server:test # run server tests (embedded SQLite, no external database)
:server:runserves on port 9000; in Docker/production the server bindsPORT(default 8080). For a full local stack identical to production, usedocker compose up --buildinstead.
./gradlew :composeApp:run # Desktop
./gradlew :composeApp:assembleDebug # Android APK
./gradlew :composeApp:wasmJsBrowserDevelopmentRun # Web (Wasm)
./gradlew :composeApp:jsBrowserDevelopmentRun # Web (JS)Open iosApp/iosApp.xcodeproj in Xcode to build for iOS.
Builds and runs the whole stack (Postgres + API + web dashboard + proxy) over plain HTTP:
cp .env.example .env # fill in values
docker compose up --build
# dashboard → http://localhost · API → http://localhost:8080Full setup guides for all targets are in crashreporter/SDK_SETUP.md:
The dashboard ships three ways, all from the same Compose Multiplatform codebase:
SITE_ADDRESS. No separate install../gradlew :composeApp:run.On first launch of the desktop/mobile app, enter your server URL and admin credentials to log in. (The web dashboard is same-origin, so it just needs your admin credentials.)
CrashWatch has two interchangeable, API-compatible backends:
server/) — the default. Runs on the JVM with PostgreSQL; this is what
the Docker images and docker-compose*.yml use.php-backend/) — a drop-in alternative for cheap shared hosting (PHP +
MySQL, no JVM/Docker required). See php-backend/README.md.Both expose the same HTTP API, so the SDK and dashboard work against either.
See CONTRIBUTING.md for how to report bugs, suggest features, and submit pull requests.
MIT — see LICENSE.