A complete example of using Contrail to index AT Protocol calendar events and RSVPs with PostgreSQL. Includes persistent Jetstream ingestion (long-running listener) and user discovery/backfill.
# Copy this folder to a new project
cp -r examples/postgres my-contrail-app
cd my-contrail-app
# Install dependencies
npm installNote: The
contraildependency inpackage.jsonpoints atgithub:flo-bit/contrail. If you're using a fork with PostgreSQL support that hasn't been merged yet, update the dependency to point at your fork's branch:npm install github:your-username/contrail#your-branchOr install from a local checkout:
npm install /path/to/your/contrail
Option A: Docker (recommended)
docker compose up -dThis starts PostgreSQL on port 5433 (to avoid conflicts with a local instance) with a contrail database ready to use. Override the port with PG_PORT=5432 docker compose up -d.
Option B: Native PostgreSQL
createdb contrailSet DATABASE_URL to point at your local instance:
export DATABASE_URL="postgresql://user:password@localhost:5432/contrail"Edit config.ts to define your collections, queryable fields, relations, and references. See the Contrail README for all options.
npm run syncThis finds users from ATProto relays and backfills their existing records from PDS. Safe to interrupt and restart — progress is saved per-DID in the database.
npm run ingestThis opens a long-lived Jetstream connection and continuously indexes new records as they appear on the network. Events are batched and flushed every 5 seconds (or every 50 events, whichever comes first). Handles reconnection automatically.
Press Ctrl+C for graceful shutdown — the current batch is flushed and the cursor is saved so the next run picks up where it left off.
npm run serveYour XRPC API is now available at http://localhost:3000:
# List events sorted by RSVP count
/xrpc/rsvp.atmo.event.listRecords?sort=rsvpsCount
# Upcoming events with 10+ going RSVPs
/xrpc/rsvp.atmo.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10
# Single event with hydrated RSVPs and profiles
/xrpc/rsvp.atmo.event.getRecord?uri=at://...&hydrateRsvps=10&profiles=true
# Search events
/xrpc/rsvp.atmo.event.listRecords?search=meetup
# RSVPs for a specific event
/xrpc/rsvp.atmo.rsvp.listRecords?subjectUri=at://...
In production you'd typically run sync once (or periodically), then keep ingest and serve running as separate processes:
# Initial sync (run once, or periodically to discover new users)
npm run sync
# In separate terminals (or use a process manager)
npm run ingest
npm run serve