Real-time global leaderboard tracking top Lightning zappers on Nostr
Built with Rails 8.1, Hotwire Turbo, PostgreSQL 16 + TimescaleDB, and pure love for Bitcoin and Nostr.
🌐 Live at: zaprank.com
- Real-time Leaderboards: All-time, 30d, 7d, and 24h top 100 zappers
- Live Auto-updates: Leaderboard refreshes every 15 seconds via Turbo Streams (no polling JS!)
- Beautiful Profile Pages: Detailed stats, charts, and zap history for each zapper
- TimescaleDB Hypertables: Optimized for 500M+ zap rows with < 80ms query times
- Permanent Zap Ingestion: Background job connects to 30+ relays and ingests kind 9735 events 24/7
- Profile Metadata Caching: Automatically fetches and caches kind 0 metadata (name, picture, nip05, lud16)
- Dark Mode First: Beautiful, mobile-perfect Tailwind UI
- Zero Redis: Everything uses Solid Queue, Solid Cache, Solid Cable (PostgreSQL-backed)
- Zero Node.js: Pure Rails 8 with importmaps
| Component | Technology |
|---|---|
| Framework | Rails 8.1 (Ruby 3.4.7) |
| Database | PostgreSQL 16 + TimescaleDB extension |
| Background Jobs | Solid Queue (PostgreSQL-backed) |
| Caching | Solid Cache (PostgreSQL-backed) |
| WebSockets | Solid Cable (PostgreSQL-backed) |
| Frontend | Hotwire Turbo + Stimulus |
| Styling | Tailwind CSS 4 (importmapped, zero Node.js) |
| Charts | Chartkick + Chart.js |
| Nostr | Faye::WebSocket for relay connections |
| Deployment | Docker + Caddy 2 (automatic HTTPS) |
Profile (profiles table)
- Caches zapper metadata: name, picture, nip05, lud16, about
- Denormalized stats: total_sats_sent, total_zaps_sent, first/last_zap_at
- Metadata TTL: 24 hours
Zap (zaps TimescaleDB hypertable)
- Partitioned by
paid_atwith 7-day chunks - Stores: event_id, zapper_pubkey, recipient_pubkey, amount_msat, bolt11, description
- Continuous aggregate:
zap_leaderboard_hourlyfor fast leaderboard queries - Unique constraint on
event_idfor idempotency
ZapIngesterJob
- Connects to 30+ high-coverage Nostr relays
- Subscribes to kind 9735 (zap receipts) in real-time
- Parses bolt11 invoices for exact amounts
- Extracts zapper pubkey from description (zap request)
- Auto-reconnects on failure
- Backfills last 10 minutes on restart
ProfileResolverJob
- Fetches kind 0 metadata from relays
- Caches for 24 hours
- Retry logic with backoff (max 5 attempts)
- Rate limiting to avoid relay abuse
- Docker & Docker Compose
- Ruby 3.4.7 (or use Docker)
- PostgreSQL 16 with TimescaleDB (or use Docker)
# 1. Clone the repository
git clone https://github.com/yourusername/zaprank.git
cd zaprank
# 2. Install dependencies
bundle install
# 3. Copy environment file
cp .env.example .env
# 4. Generate secrets
bundle exec rails secret # Copy to .env as SECRET_KEY_BASE
# 5. Start PostgreSQL with TimescaleDB (using Docker)
docker-compose up -d postgres
# 6. Setup database
bin/rails db:create db:migrate db:seed
# 7. Start Rails server
bin/dev
# 8. Start zap ingester (in another terminal)
bin/rails runner 'ZapIngesterJob.perform_now'Visit http://localhost:3000
- Ubuntu 24.04 LTS
- 4GB RAM minimum (8GB recommended)
- 50GB SSD (grows with zap data)
- Docker & Docker Compose installed
# 1. SSH into your VPS
ssh [email protected]
# 2. Install Docker (if not installed)
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
# 3. Clone repository
git clone https://github.com/yourusername/zaprank.git
cd zaprank
# 4. Setup environment
cp .env.example .env
nano .env # Fill in your values
# Generate secrets:
docker run --rm ruby:3.4-slim ruby -e "require 'securerandom'; puts SecureRandom.hex(64)"
# Copy to .env as SECRET_KEY_BASE and DATABASE_PASSWORD
# 5. Start all services
docker-compose up -d
# 6. Setup database
docker-compose exec app bin/rails db:prepare
docker-compose exec app bin/rails db:migrate
docker-compose exec app bin/rails db:seed
# 7. Check logs
docker-compose logs -f app
docker-compose logs -f worker# 1. Install Caddy
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy
# 2. Copy Caddyfile
cp Caddyfile /etc/caddy/Caddyfile
# 3. Update domain in Caddyfile
nano /etc/caddy/Caddyfile
# Change "zaprank.com" to your domain
# 4. Start Caddy
systemctl enable --now caddy
systemctl status caddyYour site should now be live at https://your-domain.com with automatic HTTPS!
TimescaleDB is automatically enabled by the init-db.sh script. The migrations create the hypertable and continuous aggregates.
With TimescaleDB hypertables and continuous aggregates:
- Leaderboard queries: < 80ms at 500M+ rows
- Daily aggregates: < 50ms
- Profile stats: < 30ms
The ingester runs as a Solid Queue job via the worker container.
# Check worker logs
docker-compose logs -f worker
# Restart worker
docker-compose restart worker# Run ingester manually
bin/rails runner 'ZapIngesterJob.perform_now'# Docker
docker-compose logs -f app
docker-compose logs -f worker
# Local
tail -f log/production.log- Nostr: The decentralized social protocol
- Lightning Network: Bitcoin's layer 2 payment network
- TimescaleDB: Time-series PostgreSQL extension
- Rails 8: The best web framework
- Hotwire: For making SPAs unnecessary
- Caddy: The best reverse proxy
Built with ⚡ and ❤️ for the Nostr community
Zap responsibly