# 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](./sqlite-bun-compat.md)) - CJS packages must be loaded via `createRequire()` (see [oauth.md](./oauth.md)) - `.env` must be loaded manually for Node ## HonoX's `noExternal: true` HonoX's Vite plugin unconditionally sets: ```ts { 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) |