sveltekit + contrail + atproto OAuth#
A sveltekit example app on cloudflare workers with self-hosted atproto record indexing via contrail, fully typed queries, and OAuth authentication.
Setup#
pnpm install
pnpm dev
Dev mode uses a loopback OAuth client — no keys or Cloudflare setup needed.
Config#
Define which AT Protocol collections to index in src/lib/contrail.config.ts:
import type { ContrailConfig } from '@atmo-dev/contrail';
export const config: ContrailConfig = {
namespace: 'statusphere.app',
collections: {
status: { // short name → URL path segment
collection: 'xyz.statusphere.status', // full NSID of the record type
queryable: {
status: {}, // equality filter (?status=...)
createdAt: { type: 'range' } // range filter (?createdAtMin=...&createdAtMax=...)
}
}
}
};
The checked-in files under lexicons/generated/ describe this example's Contrail XRPC methods. After changing those Lexicons, run pnpm generate:types to regenerate the Atcute schemas and client augmentation.
Run pnpm backfill to load existing records from the network.
Wrangler bindings (wrangler.jsonc):
- D1 (
DB) — contrail's database - KV (
OAUTH_SESSIONS,OAUTH_STATES) — OAuth session storage - Vars —
CRON_SECRET,OAUTH_PUBLIC_URL,CLIENT_ASSERTION_KEY,COOKIE_SECRET
Deploy#
pnpm wrangler d1 create statusphere
# Add database_id to wrangler.jsonc
pnpm build
pnpm wrangler deploy
How it works#
Contrail indexes AT Protocol records into D1 via Jetstream (cron, every minute). When a user posts, contrail.notify() indexes it immediately.
Typed queries use @atcute/client with an in-process handler — full type safety, zero HTTP overhead:
const client = getClient(platform!.env.DB);
const res = await client.get('statusphere.app.status.listRecords', {
params: { limit: 50, profiles: true } // typed params
});
res.data.records // typed response
pnpm generate:types uses @atcute/lex-cli to turn the checked-in method and record Lexicons into TypeScript schemas that register with @atcute/client. The generated TypeScript is committed so editors and clean builds retain typed queries.
Scheduled ingestion works around SvelteKit's lack of scheduled export support (sveltejs/kit#4841) by appending a handler post-build that self-calls /api/cron.
OAuth uses @atcute/oauth-node-client with KV-backed sessions and HMAC-signed cookies.
License#
MIT