Contrail

pre-alpha. Expect breaking changes.

a library for easily creating (serverless) atproto backends/appviews.

  • declare collections
  • get automatic jetstream backfill and ingestion, typed XRPC endpoints
  • optional: permissioned spaces and group-controlled communities

mostly tested on cloudflare workers with d1 but should run in any node env too (+ has adapters for node

and postgres for the db).

Install

pnpm add @atmo-dev/contrail

Minimal example

a complete cloudflare worker that indexes public calendar events from the atproto network and serves them over a typed XRPC endpoint. two files + config. a runnable version lives in apps/cloudflare-workers — clone, deploy, pnpm contrail backfill --remote, done.

src/contrail.config.ts — picked up automatically by the contrail CLI:

import type { ContrailConfig } from "@atmo-dev/contrail";

export const config: ContrailConfig = {
  namespace: "com.example",
  collections: {
    event: {
      collection: "community.lexicon.calendar.event", // NSID to index
      queryable: { startsAt: { type: "range" } },     // ?startsAtMin=...
      searchable: ["name", "description"],            // ?search=...
    },
  },
};

src/worker.ts — four lines. createWorker wires up fetch + scheduled + lazy init:

import { createWorker } from "@atmo-dev/contrail/worker";
import { config } from "./contrail.config";
import { lexicons } from "../lexicons/generated";

export default createWorker(config, { lexicons });

lexicons/generated/ is produced by contrail-lex generate; passing { lexicons } exposes them at /lexicons so consumer apps can typegen against your deployed service. Drop it if you don’t need that.

and a d1 binding + cron in wrangler.jsonc:

{
  "main": "src/worker.ts",
  "d1_databases": [{ "binding": "DB", "database_name": "contrail", "database_id": "..." }],
  "triggers": { "crons": ["*/1 * * * *"] }
}

then:

npx wrangler d1 create contrail   # copy the id into wrangler.jsonc
pnpm wrangler deploy              # deploy the worker
pnpm contrail backfill --remote   # one-shot historical backfill

the worker keeps itself fresh from now on via the cron. hit:

GET https://<your-worker>.workers.dev/xrpc/com.example.event.listRecords?startsAtMin=2026-01-01&limit=10

returns every community.lexicon.calendar.event record published anywhere on atproto that matches, as JSON. that’s it — no PDS setup, no lexicon publishing, no relay configuration. everything scales from there: add filters, add full-text search, add more collections, turn on spaces for private records, mount the handler in sveltekit instead, swap the adapter for postgres.

not using workers? same library, different db. see adapters for node

and postgres.

Docs

  • Indexing — the core: collections, ingestion, adapters
  • Querying — filters, sorts, hydration, search, pagination
  • Lexicons contrail-lex CLI, codegen, publishing
  • Auth — service-auth JWTs, invite tokens, watch tickets, OAuth permission sets
  • Spaces — permissioned records stored by the appview
  • Communities — group-controlled atproto DIDs
  • Sync — reactive client-side store over watchRecords
  • Labels — atproto-native moderation hydration from external labelers
  • Frameworks: SvelteKit + Cloudflare

Packages

Package
@atmo-dev/contrailCore library — indexing, XRPC server, spaces, communities, realtime
@atmo-dev/contrail-syncClient-side reactive watch-store with optional IndexedDB cache
@atmo-dev/contrail-lexiconsCodegen + contrail-lex CLI

Working in this repo? See development.md for the monorepo layout and commands.

contrail