Webhooks for the AT Protocol
airglow.run
atproto
atprotocol
automation
webhook
1# Dev server and SSR architecture
2
3Airglow 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.
4
5## How HonoX dev mode works
6
7```
8Browser request
9 ↓
10Vite dev server (Node)
11 ↓
12HonoX vite plugin (SSR middleware)
13 ↓
14app/server.ts → routes → lib/*
15 ↓
16HTML response (SSR) + client JS (HMR)
17```
18
19`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.
20
21The key constraint: **all server-side code executes in Node**, not Bun. This means:
22
23- `bun:sqlite` is not available (see [sqlite-bun-compat.md](./sqlite-bun-compat.md))
24- CJS packages must be loaded via `createRequire()` (see [oauth.md](./oauth.md))
25- `.env` must be loaded manually for Node
26
27## HonoX's `noExternal: true`
28
29HonoX's Vite plugin unconditionally sets:
30
31```ts
32{
33 ssr: {
34 noExternal: true;
35 }
36}
37```
38
39This 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()`.
40
41Workarounds:
42
43- **Vite aliases** redirect `bun:sqlite` and `better-sqlite3` to a `createRequire()` shim
44- **`createRequire()`** loads `@atproto/oauth-client-node` bypassing Vite's transform
45
46## PDS proxy
47
48The 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.
49
50A custom Vite plugin (`pdsProxy` in `vite.config.ts`) solves this:
51
52```
53Browser → http://127.0.0.1:5175/oauth/authorize
54 ↓ (Vite middleware, before HonoX)
55pdsProxy plugin
56 ↓ (http.request to localhost:3000)
57Local PDS
58 ↓ (response with pds.dev URLs rewritten to 127.0.0.1:5175)
59Browser
60```
61
62### Plugin lifecycle
63
641. 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.
652. In `configureServer`, the middleware is registered **before** HonoX's middleware (plugin order matters — `pdsProxy()` is listed first in the `plugins` array).
663. Requests matching `/oauth/`, `/.well-known/`, `/xrpc/`, `/@atproto/` are proxied.
67
68### Why `http.request()` and not `fetch()`
69
70Node's `fetch()` follows the Fetch spec and silently strips `sec-fetch-*` headers. The PDS validates these headers:
71
72- `sec-fetch-site: same-origin` — required for the PDS to accept the request
73- `sec-fetch-mode: navigate` — required for page loads (the browser sends this naturally)
74
75`http.request()` from `node:http` forwards headers as-is.
76
77### Response rewriting
78
79Text 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:
80
81- The PDS OAuth SPA's internal API calls go through the proxy
82- Redirect URLs in JSON responses point to the proxy
83- Location headers in 3xx responses are rewritten
84
85Additionally:
86
87- `Strict-Transport-Security` headers are stripped (we're on HTTP)
88- `upgrade-insecure-requests` is removed from CSP
89- `Secure` flag is stripped from cookies
90
91## Production
92
93In production, none of these workarounds apply:
94
95- `bun run start` runs the Hono app directly under Bun
96- `bun:sqlite` resolves natively
97- CJS packages are loaded by Bun's module system
98- No PDS proxy (the app and PDS are on reachable domains)
99- No `.env` loader needed (Bun auto-loads it)
100
101## Key configuration
102
103| Setting | Dev | Production |
104| ----------------- | --------------------------------- | --------------------------------- |
105| Runtime | Node (via Vite) | Bun |
106| SQLite driver | `better-sqlite3` (via alias) | `bun:sqlite` |
107| PDS access | Vite proxy + URL rewriting | Direct HTTPS |
108| `.env` loading | Manual loader in `lib/config.ts` | Bun auto-loads |
109| OAuth client type | Loopback (`http://localhost?...`) | Confidential (HTTPS metadata URL) |