Migrate webapp to SvelteKit (Svelte 5 + adapter-node) #4

Closed
opened 2026-05-14 00:06:07 +02:00 by lz · 0 comments
Owner

Goal

Migrate the entire Nexus webapp from "Fastify backend (nexus/src/) + vanilla-TS SPA (nexus/web/)" to SvelteKit (Svelte 5 + adapter-node). Same routes, same flows, same UI. No behavior changes from the operator's perspective.

Hard constraints (don't skip)

  • Auth gate must move to hooks.server.ts with a path-prefix check on /api/* + a public allowlist (/api/state, /api/auth/setup, /api/auth/unlock). Replaces nexus/src/auth/guard.ts and every app.addHook('preHandler', requireUnlocked). Audit each ported route to confirm it lands on the right side of the gate.
  • Set cookies explicitly: event.cookies.set(name, val, { path: '/', sameSite: 'lax', httpOnly: true, secure: false, maxAge: ttl }). SvelteKit's defaults differ from @fastify/cookie and silently dropping the session on redirect is the most likely regression.
  • adapter-node. Not adapter-auto. Same exposed port (3001 via PORT env).
  • pnpm test must stay green — the existing vitest suites should survive with only import-path updates, since the pure-logic modules (auth/session.ts, auth/vault-store.ts, workers/service.ts, workers/docker.ts, sessions/service.ts, connections/service.ts, crypto/*) move under src/lib/server/ without code changes.

Verification (issue is not done until all pass)

  1. pnpm install + pnpm typecheck + pnpm test + pnpm build all clean.
  2. docker compose down -v && docker compose up -d --build (new Dockerfile uses adapter-node).
  3. Full phase walkthrough from fresh state: setup → unlock → claude-session bootstrap → providers → workspaces list (with the 10s poll, skip-while-typing preserved) → new workspace → session create → session health green → lock → re-unlock.
  4. Manual curl -X POST /api/lock and /api/workers/<id>/start (bodyless POSTs — Fastify had a workaround at nexus/src/server.ts:51-56; verify SvelteKit's lazy body read covers this).

Gotchas

  • Health probe: keep per-session matching against the full --remote-control NAME from nexus/src/sessions/service.ts:checkSessionHealth. AGENTS.md fact #5 explains why — one process matching can satisfy every session's probe otherwise.
  • Bodyless POSTs: see above. Toggle endpoints (/api/lock, workspace start/stop) must not error when there's no Content-Type.
  • Migrations runner: today it's called from main() in server.ts:23. Move it to wherever you boot singletons (likely src/lib/server/singletons.ts at module init) so it runs before any route handler hits the db.
  • Graceful shutdown: adapter-node has no built-in hook. Register SIGTERM/SIGINT listeners yourself (close db, destroy SessionStore).
  • Connections view dual use: rendered both as a gating step (needs-provider, with autofocus: true) and as a regular nav target. Keep one component, take an optional autofocus prop.
  • The refresh(): Promise<void> callback that every current view receives — replace with invalidateAll() from $app/navigation, optionally exposed via setContext('refresh', ...) so children don't prop-drill.

References

  • Fastify server: nexus/src/server.ts
  • SPA bootstrap: nexus/web/src/main.ts
  • Views to port: nexus/web/src/views/*.ts
  • Auth pieces being replaced: nexus/src/auth/guard.ts, nexus/src/auth/routes.ts
## Goal Migrate the entire Nexus webapp from "Fastify backend (`nexus/src/`) + vanilla-TS SPA (`nexus/web/`)" to **SvelteKit (Svelte 5 + adapter-node)**. Same routes, same flows, same UI. No behavior changes from the operator's perspective. ## Hard constraints (don't skip) - **Auth gate must move to `hooks.server.ts`** with a path-prefix check on `/api/*` + a public allowlist (`/api/state`, `/api/auth/setup`, `/api/auth/unlock`). Replaces `nexus/src/auth/guard.ts` and every `app.addHook('preHandler', requireUnlocked)`. Audit each ported route to confirm it lands on the right side of the gate. - **Set cookies explicitly**: `event.cookies.set(name, val, { path: '/', sameSite: 'lax', httpOnly: true, secure: false, maxAge: ttl })`. SvelteKit's defaults differ from `@fastify/cookie` and silently dropping the session on redirect is the most likely regression. - **adapter-node**. Not adapter-auto. Same exposed port (3001 via `PORT` env). - **`pnpm test` must stay green** — the existing vitest suites should survive with only import-path updates, since the pure-logic modules (`auth/session.ts`, `auth/vault-store.ts`, `workers/service.ts`, `workers/docker.ts`, `sessions/service.ts`, `connections/service.ts`, `crypto/*`) move under `src/lib/server/` without code changes. ## Verification (issue is not done until all pass) 1. `pnpm install` + `pnpm typecheck` + `pnpm test` + `pnpm build` all clean. 2. `docker compose down -v && docker compose up -d --build` (new Dockerfile uses adapter-node). 3. Full phase walkthrough from fresh state: setup → unlock → claude-session bootstrap → providers → workspaces list (with the 10s poll, skip-while-typing preserved) → new workspace → session create → session health green → lock → re-unlock. 4. Manual `curl -X POST /api/lock` and `/api/workers/<id>/start` (bodyless POSTs — Fastify had a workaround at `nexus/src/server.ts:51-56`; verify SvelteKit's lazy body read covers this). ## Gotchas - **Health probe**: keep per-session matching against the full `--remote-control NAME` from `nexus/src/sessions/service.ts:checkSessionHealth`. AGENTS.md fact #5 explains why — one process matching can satisfy every session's probe otherwise. - **Bodyless POSTs**: see above. Toggle endpoints (`/api/lock`, workspace `start`/`stop`) must not error when there's no Content-Type. - **Migrations runner**: today it's called from `main()` in `server.ts:23`. Move it to wherever you boot singletons (likely `src/lib/server/singletons.ts` at module init) so it runs before any route handler hits the db. - **Graceful shutdown**: adapter-node has no built-in hook. Register `SIGTERM`/`SIGINT` listeners yourself (close db, destroy `SessionStore`). - **Connections view dual use**: rendered both as a gating step (`needs-provider`, with `autofocus: true`) and as a regular nav target. Keep one component, take an optional `autofocus` prop. - **The `refresh(): Promise<void>` callback** that every current view receives — replace with `invalidateAll()` from `$app/navigation`, optionally exposed via `setContext('refresh', ...)` so children don't prop-drill. ## References - Fastify server: `nexus/src/server.ts` - SPA bootstrap: `nexus/web/src/main.ts` - Views to port: `nexus/web/src/views/*.ts` - Auth pieces being replaced: `nexus/src/auth/guard.ts`, `nexus/src/auth/routes.ts`
lz closed this issue 2026-05-14 18:17:43 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus#4
No description provided.