Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

Dev server and SSR architecture#

Airglow uses HonoX for file-based routing with islands architecture. This creates a split-runtime situation during development: the server-side code runs on Node (via Vite), while production runs on Bun.

How HonoX dev mode works#

Browser request
    ↓
Vite dev server (Node)
    ↓
HonoX vite plugin (SSR middleware)
    ↓
app/server.ts → routes → lib/*
    ↓
HTML response (SSR) + client JS (HMR)

vp dev (Vite+) starts a Vite dev server. HonoX registers a middleware via configureServer that intercepts all requests and runs them through the Hono app using Vite's SSR module runner. Route files in app/routes/ are discovered and registered automatically.

The key constraint: all server-side code executes in Node, not Bun. This means:

  • bun:sqlite is not available (see sqlite-bun-compat.md)
  • CJS packages must be loaded via createRequire() (see oauth.md)
  • .env must be loaded manually for Node

HonoX's noExternal: true#

HonoX's Vite plugin unconditionally sets:

{
  ssr: {
    noExternal: true;
  }
}

This tells Vite to ESM-transform all node_modules during SSR instead of letting Node load them natively. This is required for HonoX's file-based routing to work (it needs to process route imports through Vite's module graph). But it breaks CJS packages that use module.exports or require().

Workarounds:

  • Vite aliases redirect bun:sqlite and better-sqlite3 to a createRequire() shim
  • createRequire() loads @atproto/oauth-client-node bypassing Vite's transform

PDS proxy#

The local PDS runs in Docker on localhost:3000 but is configured with hostname pds.dev. The browser needs to interact with the PDS (OAuth authorize page, asset loading), but pds.dev is unreachable.

A custom Vite plugin (pdsProxy in vite.config.ts) solves this:

Browser → http://127.0.0.1:5175/oauth/authorize
    ↓ (Vite middleware, before HonoX)
pdsProxy plugin
    ↓ (http.request to localhost:3000)
Local PDS
    ↓ (response with pds.dev URLs rewritten to 127.0.0.1:5175)
Browser

Plugin lifecycle#

  1. At plugin creation time, getPdsIssuer() synchronously fetches the PDS issuer URL via execSync + curl. This must be synchronous because Vite's defineConfig doesn't support async in all contexts.
  2. In configureServer, the middleware is registered before HonoX's middleware (plugin order matters — pdsProxy() is listed first in the plugins array).
  3. Requests matching /oauth/, /.well-known/, /xrpc/, /@atproto/ are proxied.

Why http.request() and not fetch()#

Node's fetch() follows the Fetch spec and silently strips sec-fetch-* headers. The PDS validates these headers:

  • sec-fetch-site: same-origin — required for the PDS to accept the request
  • sec-fetch-mode: navigate — required for page loads (the browser sends this naturally)

http.request() from node:http forwards headers as-is.

Response rewriting#

Text responses (HTML, JSON, JavaScript) have all occurrences of the PDS issuer URL (e.g. https://pds.dev) replaced with the app origin (http://127.0.0.1:5175). This ensures:

  • The PDS OAuth SPA's internal API calls go through the proxy
  • Redirect URLs in JSON responses point to the proxy
  • Location headers in 3xx responses are rewritten

Additionally:

  • Strict-Transport-Security headers are stripped (we're on HTTP)
  • upgrade-insecure-requests is removed from CSP
  • Secure flag is stripped from cookies

Production#

In production, none of these workarounds apply:

  • bun run start runs the Hono app directly under Bun
  • bun:sqlite resolves natively
  • CJS packages are loaded by Bun's module system
  • No PDS proxy (the app and PDS are on reachable domains)
  • No .env loader needed (Bun auto-loads it)

Key configuration#

Setting Dev Production
Runtime Node (via Vite) Bun
SQLite driver better-sqlite3 (via alias) bun:sqlite
PDS access Vite proxy + URL rewriting Direct HTTPS
.env loading Manual loader in lib/config.ts Bun auto-loads
OAuth client type Loopback (http://localhost?...) Confidential (HTTPS metadata URL)