···11# Airglow
2233-Webhooks for the AT Protocol. See README.md for the product spec.
33+Automations for the AT Protocol. See README.md for the product spec.
4455## Stack
66
+12-12
README.md
···11# Airglow
2233-Webhooks for the AT Protocol — subscribe to events, filter them, and forward them to HTTP endpoints.
33+Automations for the AT Protocol — listen to events, filter them, and trigger actions like webhook deliveries or PDS record creation.
4455-Airglow connects to [Jetstream](https://atproto.com/guides/streaming-data#jetstream) (AT Protocol's event streaming service), matches incoming records against user-defined subscriptions, and delivers them to callback URLs. Think IFTTT or Zapier, but the trigger side is always "something happened on the AT Protocol."
55+Airglow connects to [Jetstream](https://atproto.com/guides/streaming-data#jetstream) (AT Protocol's event streaming service), matches incoming records against user-defined automations, and executes actions automatically. Think IFTTT or Zapier, but the trigger side is always "something happened on the AT Protocol."
6677-Website: [rglw.app](https://rglw.app)
77+Website: [airglow.run](https://airglow.run)
8899## How it works
10101111### For end users
121213131. Sign in to Airglow using [AT Protocol OAuth](https://atproto.com/specs/oauth).
1414-2. Create a subscription by choosing a lexicon to listen to (e.g. `sh.tangled.feed.star`, `site.standard.document`).
1414+2. Create an automation by choosing a lexicon to listen to (e.g. `sh.tangled.feed.star`, `site.standard.document`).
15153. Add conditions to filter events — simple equality checks on record fields like the record owner or specific values in the data. The schema is known from the lexicon, so Airglow can present the available fields.
1616-4. Provide a callback URL to receive matching events.
1616+4. Add actions — deliver a webhook to a callback URL, or create a record on your PDS using a template.
17171818-Airglow verifies that the callback URL actually supports the selected lexicon before activating the subscription (see [Callback endpoints](#callback-endpoints) below).
1818+Airglow verifies that webhook callback URLs actually support the selected lexicon before activating the automation (see [Callback endpoints](#callback-endpoints) below).
19192020#### Data ownership
21212222-Subscriptions are stored on the user's PDS as [`app.rglw.subscription`](lexicons/app/rglw/subscription.json) records. The user's PDS is the source of truth — Airglow instances maintain a local index for fast event matching, but the data belongs to the user. This means subscriptions are portable across Airglow instances and visible to any AT Protocol client.
2222+Automations are stored on the user's PDS as [`run.airglow.automation`](lexicons/run/airglow/automation.json) records. The user's PDS is the source of truth — Airglow instances maintain a local index for fast event matching, but the data belongs to the user. This means automations are portable across Airglow instances and visible to any AT Protocol client.
23232424### For developers
2525···27272828#### Callback endpoints
29293030-A callback server must expose a metadata route so Airglow can discover its endpoints and verify which lexicons each one accepts:
3030+A callback server can optionally expose a metadata route so Airglow can discover its endpoints and verify which lexicons each one accepts:
31313232```
3333GET <server-base-url>/.well-known/airglow
···4444}
4545```
46464747-When a user registers a callback URL (e.g. `https://example.com/hooks/stars`), Airglow fetches the manifest from `https://example.com/.well-known/airglow` and confirms the path `/hooks/stars` is listed and accepts the requested lexicon.
4747+When a user registers a callback URL (e.g. `https://example.com/hooks/stars`), Airglow fetches the manifest from `https://example.com/.well-known/airglow`. If the manifest is present and the path is listed with the requested lexicon, the webhook is marked as **verified**. If the manifest is missing or doesn't match, the webhook is still created but shown as **unverified**. Verification is re-checked when an automation is reactivated.
48484949#### Webhook payload
50505151-When a matching event occurs, Airglow sends a POST request to the callback URL. The payload contains the Jetstream event (commit operation, record data, repo DID, timestamp) wrapped in a Airglow envelope with metadata such as the subscription ID and matched condition.
5151+When a matching event occurs, Airglow sends a POST request to the callback URL. The payload contains the Jetstream event (commit operation, record data, repo DID, timestamp) wrapped in an Airglow envelope with metadata such as the automation ID and matched condition.
52525353#### Request signing
5454···62626363### Future: protocol-native discovery
64646565-Today, users provide callback URLs manually. In the future, developers will be able to publish an `app.rglw.callback` record on their PDS, declaring their endpoint URL and supported lexicons. Airglow instances could then subscribe to this collection and index available callbacks, letting users browse and pick from discovered endpoints instead of entering URLs by hand.
6565+Today, users provide callback URLs manually. In the future, developers will be able to publish a `run.airglow.callback` record on their PDS, declaring their endpoint URL and supported lexicons. Airglow instances could then subscribe to this collection and index available callbacks, letting users browse and pick from discovered endpoints instead of entering URLs by hand.
66666767## Development
6868···102102103103```sh
104104goat lex lint lexicons/ # validate schemas
105105-goat lex new record app.rglw.<name> # create a new lexicon
105105+goat lex new record run.airglow.<name> # create a new lexicon
106106```
107107108108## Self-hosting
+4-4
app/islands/DeliveryLog.tsx
···2727 setLoading(true);
2828 setError("");
2929 try {
3030- const res = await fetch(`/api/subscriptions/${rkey}`, {
3030+ const res = await fetch(`/api/automations/${rkey}`, {
3131 method: "PATCH",
3232 headers: { "Content-Type": "application/json" },
3333 body: JSON.stringify({ active: !isActive }),
···4646 }, [rkey, isActive]);
47474848 const handleDelete = useCallback(async () => {
4949- if (!confirm("Delete this subscription? This cannot be undone.")) return;
4949+ if (!confirm("Delete this automation? This cannot be undone.")) return;
5050 setLoading(true);
5151 setError("");
5252 try {
5353- const res = await fetch(`/api/subscriptions/${rkey}`, {
5353+ const res = await fetch(`/api/automations/${rkey}`, {
5454 method: "DELETE",
5555 });
5656 if (!res.ok) {
···68686969 const refreshLogs = useCallback(async () => {
7070 try {
7171- const res = await fetch(`/api/subscriptions/${rkey}`);
7171+ const res = await fetch(`/api/automations/${rkey}`);
7272 if (res.ok) {
7373 const data = await res.json();
7474 setLogs(data.deliveryLogs || []);
···1414 <AppShell header={<Header user={user} actions={<ThemeToggle />} />}>
1515 <Container>
1616 <section class={s.hero}>
1717- <h1 class={s.heroTitle}>Webhooks for the AT Protocol</h1>
1717+ <h1 class={s.heroTitle}>Automations for the AT Protocol</h1>
1818 <p class={s.heroSubtitle}>
1919- Subscribe to events across the AT Protocol network and receive real-time webhook
2020- deliveries. Filter by lexicon, match conditions, and track every delivery.
1919+ Listen to events across the AT Protocol network and trigger actions automatically.
2020+ Filter by lexicon, deliver webhooks, create records on your PDS, and track every run.
2121 </p>
2222 {user ? (
2323 <Button href="/dashboard" size="lg">
···32323333 <section class={s.features}>
3434 <div class={s.featureCard}>
3535- <h3 class={s.featureTitle}>Real-time Webhooks</h3>
3535+ <h3 class={s.featureTitle}>Webhook Delivery</h3>
3636 <p class={s.featureDesc}>
3737 Receive HTTP POST callbacks instantly when matching events occur on the AT Protocol
3838 network via Jetstream.
3939 </p>
4040 </div>
4141 <div class={s.featureCard}>
4242- <h3 class={s.featureTitle}>Lexicon Filtering</h3>
4242+ <h3 class={s.featureTitle}>Record Creation</h3>
4343 <p class={s.featureDesc}>
4444- Subscribe to specific record types by NSID. Add field-level conditions to match
4545- exactly the events you need.
4444+ Automatically create records on your PDS when events match. Use templates with
4545+ placeholders to build records from event data.
4646 </p>
4747 </div>
4848 <div class={s.featureCard}>
4949- <h3 class={s.featureTitle}>Delivery Tracking</h3>
4949+ <h3 class={s.featureTitle}>Smart Filtering</h3>
5050 <p class={s.featureDesc}>
5151- Full delivery log with status codes, retry attempts, and error details. Know exactly
5252- what happened with every event.
5151+ Listen to specific record types by NSID. Add field-level conditions with operators
5252+ like equals, starts with, or contains.
5353 </p>
5454 </div>
5555 <div class={s.featureCard}>
5656- <h3 class={s.featureTitle}>HMAC Signing</h3>
5656+ <h3 class={s.featureTitle}>Delivery Tracking</h3>
5757 <p class={s.featureDesc}>
5858- Every webhook is signed with a per-subscription HMAC secret so your callback can
5959- verify authenticity.
5858+ Full delivery log with status codes, retry attempts, and error details. Know exactly
5959+ what happened with every event.
6060 </p>
6161 </div>
6262 </section>
···7171 </li>
7272 <li class={s.step}>
7373 <div class={s.stepNumber}>2</div>
7474- <h3 class={s.stepTitle}>Subscribe</h3>
7474+ <h3 class={s.stepTitle}>Automate</h3>
7575 <p class={s.stepDesc}>
7676- Choose a lexicon, set conditions, and provide your callback URL.
7676+ Choose a lexicon, set conditions, and add actions like webhooks or record creation.
7777 </p>
7878 </li>
7979 <li class={s.step}>
8080 <div class={s.stepNumber}>3</div>
8181 <h3 class={s.stepTitle}>Receive</h3>
8282 <p class={s.stepDesc}>
8383- Get signed webhook deliveries in real time with automatic retries.
8383+ Get signed webhook deliveries and automatic record creation in real time with
8484+ retries.
8485 </p>
8586 </li>
8687 </ol>
8788 </section>
8889 </Container>
8990 </AppShell>,
9090- { title: "Airglow — Webhooks for the AT Protocol" },
9191+ { title: "Airglow — Automations for the AT Protocol" },
9192 );
9293});
···11+CREATE TABLE `automations` (
22+ `uri` text PRIMARY KEY NOT NULL,
33+ `did` text NOT NULL,
44+ `rkey` text NOT NULL,
55+ `name` text NOT NULL,
66+ `description` text,
77+ `lexicon` text NOT NULL,
88+ `actions` text DEFAULT '[]' NOT NULL,
99+ `fetches` text DEFAULT '[]' NOT NULL,
1010+ `conditions` text DEFAULT '[]' NOT NULL,
1111+ `active` integer DEFAULT false NOT NULL,
1212+ `indexed_at` integer NOT NULL
1313+);
1414+--> statement-breakpoint
115CREATE TABLE `delivery_logs` (
216 `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
33- `subscription_uri` text NOT NULL,
1717+ `automation_uri` text NOT NULL,
418 `action_index` integer DEFAULT 0 NOT NULL,
519 `event_time_us` integer NOT NULL,
620 `payload` text,
···822 `error` text,
923 `attempt` integer DEFAULT 1 NOT NULL,
1024 `created_at` integer NOT NULL,
1111- FOREIGN KEY (`subscription_uri`) REFERENCES `subscriptions`(`uri`) ON UPDATE no action ON DELETE cascade
2525+ FOREIGN KEY (`automation_uri`) REFERENCES `automations`(`uri`) ON UPDATE no action ON DELETE cascade
1226);
1327--> statement-breakpoint
1428CREATE TABLE `lexicon_cache` (
···2741 `key` text PRIMARY KEY NOT NULL,
2842 `value` text NOT NULL,
2943 `expires_at` integer
3030-);
3131---> statement-breakpoint
3232-CREATE TABLE `subscriptions` (
3333- `uri` text PRIMARY KEY NOT NULL,
3434- `did` text NOT NULL,
3535- `rkey` text NOT NULL,
3636- `name` text NOT NULL,
3737- `description` text,
3838- `lexicon` text NOT NULL,
3939- `actions` text DEFAULT '[]' NOT NULL,
4040- `fetches` text DEFAULT '[]' NOT NULL,
4141- `conditions` text DEFAULT '[]' NOT NULL,
4242- `active` integer DEFAULT false NOT NULL,
4343- `indexed_at` integer NOT NULL
4444);
4545--> statement-breakpoint
4646CREATE TABLE `users` (