Upgrade Notes

v0.3 — Security Hardening

This release contains breaking changes that require attention before upgrading.

Breaking Changes

ChangeImpactAction 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

Production Checklist

Before going live, work through this checklist:

Security

ItemActionWhy
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

ItemActionWhy
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

ItemDefaultGuidance
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

bash
# 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

  1. Go to railway.app/new and click Deploy from GitHub repo
  2. Select your Bloop fork (or use the template repo)
  3. Railway auto-detects the Dockerfile and begins building

Step 2: Add a Persistent Volume

This is critical — without a volume, your database is lost on every deploy.

  1. In your service settings, go to Volumes
  2. Click Add Volume
  3. Set the mount path to /data
  4. 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:

VariableValueNotes
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:

VariablePurpose
BLOOP_SLACK_WEBHOOK_URLGlobal Slack webhook for alerts (fallback for rules without channels)
BLOOP_WEBHOOK_URLGlobal generic webhook URL
BLOOP__RATE_LIMIT__PER_SECONDIngestion rate limit per IP (default: 100)
BLOOP__RETENTION__RAW_EVENTS_DAYSHow long to keep raw events (default: 7)

Step 4: Add a Custom Domain

  1. In Settings → Networking → Custom Domain, enter your domain (e.g., errors.yourapp.com)
  2. Railway provides the CNAME target — add it to your DNS
  3. Railway provisions a TLS certificate automatically
  4. 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

  1. In Settings → Deploy, set the health check path to /health
  2. Set timeout to 10 seconds
  3. Railway will wait for a 200 response before routing traffic to the new deployment

Step 6: First Login & Project Setup

  1. Visit https://errors.yourapp.com — you'll see the passkey registration screen
  2. Register your passkey (fingerprint, Face ID, or hardware key)
  3. The first user is automatically an admin
  4. Go to Settings → Projects — a default project is created automatically
  5. Create additional projects for each app/service
  6. 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:

bash
# 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:

MetricIdleModerate 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

  1. Add application: In your Dokploy dashboard, create a new application from your Git repository.
  2. Build configuration: Select "Dockerfile" as the build method. Dokploy will use the Dockerfile in the repo root.
  3. Environment variables: Add the same variables listed in the Railway guide in the Environment tab.
  4. Volume mount: Add a persistent volume:
    yaml
    Host path:      /opt/dokploy/volumes/bloop
    Container path: /data
  5. Domain & SSL: Add your domain in the Domains tab. Enable "Generate SSL" for automatic Let's Encrypt certificates. Set the container port to 5332.
  6. Health check: Set the health check path to /health and port to 5332.

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

bash
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

yaml
# 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:

nginx
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:

bash
# 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

bash
#!/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)"
bash
# 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:

bash
# 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

bash
# 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.