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:sqliteis not available (see sqlite-bun-compat.md)- CJS packages must be loaded via
createRequire()(see oauth.md) .envmust 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:sqliteandbetter-sqlite3to acreateRequire()shim createRequire()loads@atproto/oauth-client-nodebypassing 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#
- At plugin creation time,
getPdsIssuer()synchronously fetches the PDS issuer URL viaexecSync+curl. This must be synchronous because Vite'sdefineConfigdoesn't support async in all contexts. - In
configureServer, the middleware is registered before HonoX's middleware (plugin order matters —pdsProxy()is listed first in thepluginsarray). - 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 requestsec-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-Securityheaders are stripped (we're on HTTP)upgrade-insecure-requestsis removed from CSPSecureflag is stripped from cookies
Production#
In production, none of these workarounds apply:
bun run startruns the Hono app directly under Bunbun:sqliteresolves natively- CJS packages are loaded by Bun's module system
- No PDS proxy (the app and PDS are on reachable domains)
- No
.envloader 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) |