Upgrade Notes
v0.3 — Security Hardening
This release contains breaking changes that require attention before upgrading.
Breaking Changes
| Change | Impact | Action Required |
|---|---|---|
| HMAC secret minimum length | Server refuses to start if auth.hmac_secret is shorter than 32 characters. |
Before deploying, ensure your secret is ≥ 32 chars. Generate one with openssl rand -hex 32. |
| Session tokens hashed at rest | All existing sessions are invalidated on upgrade. Users must re-login via WebAuthn. | Schedule the upgrade during a low-traffic window. No data migration needed — old sessions are cleaned up automatically (migration 010). |
api_key removed from project list/get |
GET /v1/projects and GET /v1/projects/:slug no longer return the api_key field. API keys are only returned on POST /v1/projects (create) and POST /v1/projects/:slug/rotate-key. |
Update any scripts or dashboards that read api_key from GET responses. Capture the key on project creation or use rotate-key to obtain it. |
| Auth endpoint rate limiting | Login and registration endpoints are rate-limited to 5 req/sec per IP (configurable via rate_limit.auth_per_second and rate_limit.auth_burst_size). |
No action needed for normal usage. Adjust config values if your deployment has unusual auth traffic patterns. |
| Token route authorization | Token CRUD routes now require the user to be an admin, the project creator, or (for revocation) the token creator. | No action needed unless non-admin users were managing tokens for projects they didn't create. |
Security Fixes
- C-1: Session tokens are now SHA-256 hashed before database storage (prevents token theft on DB breach).
- H-1: Token CRUD routes enforce project membership (admin or project creator).
- H-2: API keys are no longer exposed in project list/get API responses.
- H-3: Bearer token cache re-validates
expires_aton every hit (prevents expired token reuse). - H-4:
/v1/releases/:release/errorsnow filters byproject_id(prevents cross-project data leakage). - H-7: Auth endpoints rate-limited to prevent brute-force attacks.
- H-8: HMAC secrets shorter than 32 characters are rejected at startup.
Production Checklist
Before going live, work through this checklist:
Security
| Item | Action | Why |
|---|---|---|
| HMAC secret | Set BLOOP__AUTH__HMAC_SECRET to a random 64+ character string |
This is the signing key for all SDK authentication. Use openssl rand -hex 32 to generate one. |
| WebAuthn origin | Set BLOOP__AUTH__RP_ID and BLOOP__AUTH__RP_ORIGIN to your domain |
Passkey registration fails silently if these don't match the browser's actual origin. |
| HTTPS | Always use TLS in production | WebAuthn requires a secure context. API keys and session cookies are transmitted in headers. |
| Project API keys | Create separate projects per app/service | Isolates errors and allows key rotation without affecting other services. |
Storage
| Item | Action | Why |
|---|---|---|
| Persistent volume | Mount a volume at /data |
SQLite stores everything in a single file. Without a volume, data is lost on container restart. |
| Retention policy | Set retention.raw_events_days (default: 7) |
Raw events are pruned after this period. Aggregates and samples are kept indefinitely. |
| Backups | Schedule regular SQLite backups | See Backups section below for a cron-based approach. |
Performance
| Item | Default | Guidance |
|---|---|---|
ingest.channel_capacity |
8192 | Buffer size for incoming events. Increase if you see buffer_usage > 0.5 on /health. |
pipeline.flush_batch_size |
500 | Events written per batch. Higher = better throughput, more write latency. |
database.pool_size |
4 | Read connections. Increase for high query concurrency (dashboard + API). |
rate_limit.per_second |
100 | Per-IP rate limit. Increase if a single server sends high volume legitimately. |
Monitoring
# Health check — use this for uptime monitoring
curl -sf https://errors.yourapp.com/health | jq .
# Expected response:
# { "status": "ok", "db_ok": true, "buffer_usage": 0.002 }
# Alert if buffer_usage > 0.5 or db_ok is false
Deploy to Railway
Railway is the fastest way to deploy Bloop. This guide walks through a complete production setup.
Step 1: Create the Project
- Go to railway.app/new and click Deploy from GitHub repo
- Select your Bloop fork (or use the template repo)
- Railway auto-detects the
Dockerfileand begins building
Step 2: Add a Persistent Volume
This is critical — without a volume, your database is lost on every deploy.
- In your service settings, go to Volumes
- Click Add Volume
- Set the mount path to
/data - Railway provisions the volume automatically (default 10 GB, expandable)
Do this before the first deploy completes. If Bloop starts without a volume, it creates the database in the ephemeral container filesystem, and you'll lose it on the next deploy.
Step 3: Set Environment Variables
Go to Variables in your service and add each of these:
| Variable | Value | Notes |
|---|---|---|
BLOOP__AUTH__HMAC_SECRET |
A random 64+ char string | Generate with openssl rand -hex 32. This is your master signing key. |
BLOOP__AUTH__RP_ID |
errors.yourapp.com |
Must match your custom domain exactly (no protocol, no port). |
BLOOP__AUTH__RP_ORIGIN |
https://errors.yourapp.com |
Full URL with https://. Must match what the browser sees. |
BLOOP__DATABASE__PATH |
/data/bloop.db |
Points to your persistent volume. |
RUST_LOG |
bloop=info |
Use bloop=debug for troubleshooting. |
Optional variables:
| Variable | Purpose |
|---|---|
BLOOP_SLACK_WEBHOOK_URL | Global Slack webhook for alerts (fallback for rules without channels) |
BLOOP_WEBHOOK_URL | Global generic webhook URL |
BLOOP__RATE_LIMIT__PER_SECOND | Ingestion rate limit per IP (default: 100) |
BLOOP__RETENTION__RAW_EVENTS_DAYS | How long to keep raw events (default: 7) |
Step 4: Add a Custom Domain
- In Settings → Networking → Custom Domain, enter your domain (e.g.,
errors.yourapp.com) - Railway provides the CNAME target — add it to your DNS
- Railway provisions a TLS certificate automatically
- Wait for DNS propagation (usually 1-5 minutes)
The domain must be configured before you register your first passkey. WebAuthn binds credentials to the origin. If you register on *.up.railway.app and later switch to a custom domain, those passkeys won't work on the new domain.
Step 5: Configure Health Check
- In Settings → Deploy, set the health check path to
/health - Set timeout to
10seconds - Railway will wait for a 200 response before routing traffic to the new deployment
Step 6: First Login & Project Setup
- Visit
https://errors.yourapp.com— you'll see the passkey registration screen - Register your passkey (fingerprint, Face ID, or hardware key)
- The first user is automatically an admin
- Go to Settings → Projects — a default project is created automatically
- Create additional projects for each app/service
- Copy the API key and follow the SDK snippets shown below each project
Step 7: Verify Integration
Send a test error to confirm everything is working:
# Replace with your actual values
API_KEY="your-project-api-key"
ENDPOINT="https://errors.yourapp.com"
BODY='{"timestamp":'$(date +%s)',"source":"api","environment":"production","release":"1.0.0","error_type":"TestError","message":"Hello from Bloop!"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$API_KEY" | awk '{print $2}')
curl -s -w "\nHTTP %{http_code}\n" \
-X POST "$ENDPOINT/v1/ingest" \
-H "Content-Type: application/json" \
-H "X-Signature: $SIG" \
-H "X-Project-Key: $API_KEY" \
-d "$BODY"
# Expected: HTTP 200
# Check the dashboard — your test error should appear within 2 seconds
Railway Resource Usage
Bloop is lightweight. Typical Railway resource usage:
| Metric | Idle | Moderate load (1k events/min) |
|---|---|---|
| Memory | ~20 MB | ~50 MB |
| CPU | < 1% | ~5% |
| Disk | ~15 MB (binary) | Depends on volume and retention |
Railway's Hobby plan ($5/month) is more than sufficient for most use cases.
Deploy to Dokploy
- Add application: In your Dokploy dashboard, create a new application from your Git repository.
- Build configuration: Select "Dockerfile" as the build method. Dokploy will use the Dockerfile in the repo root.
- Environment variables: Add the same variables listed in the Railway guide in the Environment tab.
-
Volume mount: Add a persistent volume:
yaml
Host path: /opt/dokploy/volumes/bloop Container path: /data -
Domain & SSL: Add your domain in the Domains tab. Enable "Generate SSL" for automatic Let's Encrypt certificates. Set the container port to
5332. -
Health check: Set the health check path to
/healthand port to5332.
Then follow Steps 6 and 7 from the Railway guide for first login and verification.
Deploy to Docker / VPS
Run Bloop on any server with Docker installed.
Quick Start
docker run -d --name bloop \
--restart unless-stopped \
-p 5332:5332 \
-v bloop_data:/data \
-e BLOOP__AUTH__HMAC_SECRET=$(openssl rand -hex 32) \
-e BLOOP__AUTH__RP_ID=errors.yourapp.com \
-e BLOOP__AUTH__RP_ORIGIN=https://errors.yourapp.com \
-e BLOOP__DATABASE__PATH=/data/bloop.db \
-e RUST_LOG=bloop=info \
ghcr.io/jaikoo/bloop:latest
With Docker Compose
# docker-compose.yml
services:
bloop:
image: ghcr.io/jaikoo/bloop:latest
restart: unless-stopped
ports:
- "5332:5332"
volumes:
- bloop_data:/data
environment:
BLOOP__AUTH__HMAC_SECRET: "${BLOOP_SECRET}"
BLOOP__AUTH__RP_ID: errors.yourapp.com
BLOOP__AUTH__RP_ORIGIN: https://errors.yourapp.com
BLOOP__DATABASE__PATH: /data/bloop.db
RUST_LOG: bloop=info
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5332/health"]
interval: 30s
timeout: 5s
retries: 3
volumes:
bloop_data:
Reverse Proxy (Nginx)
Place Bloop behind a reverse proxy for TLS termination:
server {
listen 443 ssl http2;
server_name errors.yourapp.com;
ssl_certificate /etc/letsencrypt/live/errors.yourapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/errors.yourapp.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5332;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Important for large source map uploads
client_max_body_size 50m;
}
}
If using Caddy instead, it handles TLS automatically: errors.yourapp.com { reverse_proxy localhost:5332 }
Backups
Bloop stores all data in a single SQLite file. Back it up with a simple copy or the SQLite .backup command.
Safe Hot Backup
SQLite's .backup command creates a consistent snapshot without stopping the server:
# From the host (Docker)
docker exec bloop sqlite3 /data/bloop.db ".backup /data/backup.db"
docker cp bloop:/data/backup.db ./bloop-backup-$(date +%Y%m%d).db
# Or directly on the volume
sqlite3 /var/lib/docker/volumes/bloop_data/_data/bloop.db \
".backup /tmp/bloop-backup.db"
Automated Daily Backups
#!/bin/bash
# /opt/bloop/backup.sh — run via cron
BACKUP_DIR="/opt/bloop/backups"
KEEP_DAYS=14
mkdir -p "$BACKUP_DIR"
# Create backup
docker exec bloop sqlite3 /data/bloop.db \
".backup /data/backup-tmp.db"
docker cp bloop:/data/backup-tmp.db \
"$BACKUP_DIR/bloop-$(date +%Y%m%d-%H%M).db"
docker exec bloop rm /data/backup-tmp.db
# Prune old backups
find "$BACKUP_DIR" -name "bloop-*.db" -mtime +$KEEP_DAYS -delete
echo "Backup complete: $(ls -lh $BACKUP_DIR/bloop-*.db | tail -1)"
# Add to crontab: daily at 3 AM
echo "0 3 * * * /opt/bloop/backup.sh >> /var/log/bloop-backup.log 2>&1" | crontab -
Railway Backups
Railway volumes support snapshots. You can also back up by running a one-off command:
# Connect to Railway service shell
railway shell
# Inside the container
sqlite3 /data/bloop.db ".backup /data/bloop-backup.db"
# Download via Railway CLI
railway volume download /data/bloop-backup.db
Restoring from Backup
# Stop the server first
docker stop bloop
# Replace the database
docker cp ./bloop-backup-20240115.db bloop:/data/bloop.db
# Restart
docker start bloop
Always stop the server before restoring. SQLite can corrupt if you overwrite a database file while it's in use. The WAL journal (bloop.db-wal) must also be consistent — using .backup ensures this.