Private crash reporting for apps that respect their users.
End-to-end encrypted. User-consented. Self-hostable as a single binary. No third party ever sees your crash data — not the relay, not a SaaS vendor, not Google.
| Bugstr | Sentry | Crashlytics | |
|---|---|---|---|
| Who sees crash data? | Only you | Sentry servers | |
| E2E encrypted? | Yes (NIP-44) | No | No |
| User consent before sending? | Yes (opt-in) | No | No |
| Data retention | Auto-expires (30 days) | Indefinite | 90 days |
| Vendor lock-in | None (open protocol) | Their infrastructure | Firebase |
| Self-hostable? | Single binary | 10+ Docker services | No |
| Cost to get started | Free | Free (limited) | Free (Google ToS) |
Crash → Cache locally → App restart → User consents → Encrypted report sent → Auto-expires in 30 days
- Crash occurs — Exception handler captures stack trace, saved to disk. No network call.
- User decides — On next launch, a consent dialog lets the user choose whether to send. No data leaves the device without explicit approval.
- Encrypted delivery — Report is encrypted end-to-end with NIP-44 and gift-wrapped via NIP-59. The relay transporting it cannot read the content.
- You decrypt — Only your Nostr private key can decrypt the report. No third party ever sees it.
- Auto-expiration — Reports are deleted from relays after 30 days via NIP-40.
- No PII collected by default — No IP addresses, emails, device IDs, or usernames
- Opt-in, not opt-out — Users explicitly consent before any data leaves their device
- End-to-end encrypted — NIP-44 (ChaCha20, HMAC-SHA256, Cure53 audited)
- Zero-knowledge relay — The relay transports ciphertext. It never sees plaintext crash data.
- Ephemeral sender identity — Each report uses a fresh keypair. Reports cannot be correlated to a user.
- Auto-expiring — Reports self-delete from relays after 30 days
- Open source — Every line of code is auditable
- No vendor lock-in — Built on the open Nostr protocol. If bugstr disappears, the protocol still works.
| Platform | Directory | Tested |
|---|---|---|
| Android/Kotlin | android/ |
✅ Zapstore |
| Electron | electron/ |
Community testing |
| Flutter/Dart | dart/ |
Community testing |
| Rust | rust/ |
Community testing |
| Go | go/ |
Community testing |
| Python | python/ |
Community testing |
| React Native | react-native/ |
Community testing |
The Rust receiver includes a web dashboard with:
- Fingerprint-based crash grouping — Crashes grouped by stack trace similarity (Rollbar-style algorithm), not just exception type
- Server-side symbolication — Supports Android (ProGuard/R8), Flutter, Electron/JS, React Native, Rust, Go, Python
- Human-readable group titles — e.g. "StateError in _AboutSection.build (profile_screen.dart)"
- Single binary — No Docker, no Kafka, no Redis. Just
bugstr serve.
# Start the receiver
bugstr serve --mappings ./mappings
# With symbolication for Android
bugstr symbolicate --platform android --input crash.txt \
--app-id com.example.app --version 1.0.0| Relay | Notes |
|---|---|
wss://relay.damus.io |
strfry defaults |
wss://relay.primal.net |
strfry defaults |
wss://nos.lol |
Fallback relay |
Override via the relays configuration option in each SDK.
All implementations use these NIPs:
- NIP-17 — Private Direct Messages (kind 14 rumors)
- NIP-44 — Versioned Encryption (v2, ChaCha20 + HMAC-SHA256)
- NIP-59 — Gift Wrap (rumor → seal → gift wrap)
- NIP-40 — Expiration Timestamp
Per NIP-17, rumors (kind 14) must include:
id— SHA256 hash of[0, pubkey, created_at, kind, tags, content]sig: ""— Empty string (not omitted)
Most relays enforce a 64 KB event size limit. Bugstr handles this automatically:
| Payload Size | Behavior |
|---|---|
| < 1 KB | Sent as plain JSON |
| >= 1 KB | Compressed with gzip, base64-encoded |
Gzip achieves 70-90% reduction on stack traces. With compression, most crash reports fit well within relay limits.
Compression format details
Large payloads are wrapped in a versioned envelope:
{
"v": 1,
"compression": "gzip",
"payload": "<base64-encoded-gzip-data>"
}Stack traces are automatically truncated to fit within limits (default: 200 KB before compression). The receiver automatically detects and decompresses payloads.
| Original Size | Compressed | Reduction |
|---|---|---|
| 10 KB | ~1-2 KB | ~80-90% |
| 50 KB | ~5-10 KB | ~80-90% |
| 200 KB | ~20-40 KB | ~80-85% |
The Rust receiver includes built-in symbolication for 7 platforms:
| Platform | Mapping File | Notes |
|---|---|---|
| Android | mapping.txt |
ProGuard/R8 with full line-range support |
| Electron/JS | *.js.map |
Source map v3 |
| Flutter | *.symbols |
Via flutter symbolize or direct parsing |
| Rust | Backtrace | Debug builds include source locations |
| Go | Goroutine stacks | Symbol tables usually embedded |
| Python | Tracebacks | Source file mapping |
| React Native | *.bundle.map |
Hermes bytecode + JS source maps |
Mapping file organization
mappings/
android/
com.example.app/
1.0.0/mapping.txt
1.1.0/mapping.txt
electron/
my-app/
1.0.0/main.js.map
flutter/
com.example.app/
1.0.0/app.android-arm64.symbols
The receiver automatically falls back to the newest available version if an exact version match isn't found.
The test-vectors/ directory contains JSON test cases for NIP-17 and crash payload conformance. All platform implementations should validate against these vectors.
See AGENTS.md for contributor guidelines covering:
- Privacy requirements and PII rules
- Crash report payload schema
- Fingerprint algorithm specification
- Documentation and commit conventions