❤️ favs.blue#
A favstar.fm-style web app for Bluesky: type any handle and see that user's most-liked or most-reposted posts, filterable by "all time" or "last month."
Live engagement comes from the Bluesky Jetstream firehose. The first time a handle is looked up, a backfill job pulls historical posts from the Bluesky AppView API.
Stack: TypeScript / AdonisJS, ClickHouse for engagement events, SQLite for metadata, Edge.js templates with Alpine.js (CSP build) on the frontend. Ships as one Docker image with four process entrypoints (web, jetstream consumer, queue worker, scheduler).
Local development#
Prerequisites: Mise, Docker.
# 1. Install Node/pnpm
mise trust && mise install
# 1. Copy the env template and generate an APP_KEY
cp .env.example .env
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
# paste the output into APP_KEY in .env
# 2. Start ClickHouse
docker compose up clickhouse -d
# 3. Install dependencies
pnpm install
# 4. Run migrations (SQLite + ClickHouse) and start all four processes
pnpm dev
pnpm dev runs the web server, jetstream consumer, queue worker, and scheduler concurrently with hot-reload. The app is at http://localhost:3333.
The first profile lookup for a new handle takes a minute or two while backfill runs. Live engagement updates flow in once the jetstream consumer has been running.
Useful commands#
pnpm test # Japa test runner
pnpm lint # eslint
pnpm typecheck # tsc --noEmit
pnpm format # prettier --write
pnpm build # production build
# Run a single test file
node ace test --files "tests/unit/clickhouse_store.spec.ts"
# Apply ClickHouse migrations only
node ace clickhouse:migrate
Production (Docker Swarm)#
The repository ships a docker-compose.yml configured for docker stack deploy.
Architecture#
Three process entrypoints:
- HTTP web server (
bin/server.ts) — controllers inapp/controllers/, routes instart/routes.ts. Profile pages at/profile/:handle/likes|repostsare the main product surface. - Jetstream consumer (
node ace jetstream:consume) — connects to the Bluesky Jetstream WebSocket, filters for tracked DIDs, writes engagement events to ClickHouse, and persists cursor position so it can resume after restart. - Queue worker (
node ace queue:work) — runs backfill jobs that fetch historical posts/likes/reposts from the AppView on first lookup of a new handle.
A scheduler process (node ace scheduler:run) handles periodic jobs like virality threshold scans.
Configuration#
See .env.example for the full list of environment variables. start/env.ts is the source of truth — variables are validated there at boot.
License#
See LICENSE.