# Task 1.3 — Vite dev proxy + path aliases + tsconfig hardening **Phase:** 1 — Foundation **Status:** ⬜ Not started **Depends on:** 1.2 **Wiki refs:** `docs/wiki/entities/react-spa.md` §Auth pattern ## Goal Configure Vite's dev server to proxy `/api`, `/ws-business`, `/ws-live`, and `/config.json` to the appropriate local services so the SPA dev experience matches the stage/prod reverse-proxy topology. Same-origin in dev means cookies flow correctly, identical to stage/prod where Traefik does the routing. Also: add path aliases (`@/`) so imports stay short, and tighten `tsconfig.json` for stricter type safety. After this task, `pnpm dev` against a locally-running Directus + (eventually) Processor lets the SPA exercise the real auth flow without CORS workarounds. ## Deliverables - `vite.config.ts` updated: - `server.proxy` configured for: - `/api` → `http://localhost:8055` (Directus REST/GraphQL). - `/ws-business` → `ws://localhost:8055/websocket` (Directus business-plane WS), with `ws: true` and `rewrite` to strip `/ws-business`. - `/ws-live` → `ws://localhost:8081` (Processor live WS, once Phase 1.5 lands), with `ws: true` and `rewrite` to strip `/ws-live`. - Don't proxy `/config.json` — it's served from `public/` by Vite directly in dev. - `resolve.alias` for `@/` → `src/`. - `server.port` pinned to `5173` (Vite default; mention in README so anyone running other Vite apps avoids the conflict). - `tsconfig.app.json` updated: - `compilerOptions.baseUrl` to `.`, `paths` to `{ "@/*": ["src/*"] }`. - Add `noUncheckedIndexedAccess: true` (matches `tcp-ingestion` / `processor` strictness). - Add `noImplicitOverride: true`. - `verbatimModuleSyntax: true` is fine if already on; otherwise leave for now. - `README.md` updated: a "local dev" section listing the assumed local services + how to override the proxy targets via env vars (next bullet). - Optional: env-var override for proxy targets: - `VITE_DEV_DIRECTUS_URL` (default `http://localhost:8055`). - `VITE_DEV_PROCESSOR_WS_URL` (default `ws://localhost:8081`). - Read in `vite.config.ts` via `loadEnv`. - Document in `.env.dev.example` (committed) and `.env.dev.local` (gitignored). ## Specification ### Why proxy in dev instead of CORS CORS-with-credentials requires: 1. The browser sending `withCredentials: true` on every request. 2. The server responding with `Access-Control-Allow-Origin: ` (not `*`) and `Access-Control-Allow-Credentials: true`. 3. Cookies being scoped correctly (`SameSite=None; Secure` for cross-site — which means HTTPS even in dev). This works but it's three places that must agree, and `SameSite=None; Secure` requires localhost-with-TLS in dev (extra setup). The dev proxy collapses all of it into "everything is one origin," matching stage/prod's reverse-proxy topology. **Pick the dev proxy.** ### Proxy config shape ```ts // vite.config.ts import { defineConfig, loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import path from 'node:path'; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), 'VITE_'); const directusUrl = env.VITE_DEV_DIRECTUS_URL ?? 'http://localhost:8055'; const processorWsUrl = env.VITE_DEV_PROCESSOR_WS_URL ?? 'ws://localhost:8081'; return { plugins: [react(), tailwindcss()], resolve: { alias: { '@': path.resolve(__dirname, './src') }, }, server: { port: 5173, proxy: { '/api': { target: directusUrl, changeOrigin: true, rewrite: (p) => p.replace(/^\/api/, ''), }, '/ws-business': { target: directusUrl, ws: true, changeOrigin: true, rewrite: (p) => p.replace(/^\/ws-business/, '/websocket'), }, '/ws-live': { target: processorWsUrl, ws: true, changeOrigin: true, rewrite: (p) => p.replace(/^\/ws-live/, ''), }, }, }, }; }); ``` The path namespacing (`/api`, `/ws-business`, `/ws-live`) is what the SPA code calls. The proxy strips the namespace and forwards. Stage/prod's Traefik does the same routing under the public domain — same paths, same code, no env branching in the SPA. ### Path alias `@/` is the convention shadcn/ui assumes (its `components.json` config wires `@/components/...`, `@/lib/utils`). Aligning Vite + tsconfig with it means imports look like: ```ts import { Button } from '@/ui/primitives/button'; import { useAuthStore } from '@/auth/store'; ``` instead of `../../../ui/primitives/button`. Don't add other top-level aliases — `@/` for `src/` is enough. ### Why `noUncheckedIndexedAccess` Without it, `arr[0]` is typed as `T` even when the array might be empty. With it, `arr[0]` is typed `T | undefined`, forcing a check. Catches a real class of bugs in map-position code (where "first device" assumptions are easy to make and easy to get wrong when the array is empty during reconnect). The processor and tcp-ingestion both have this on. Match. ### Vite proxy and WebSocket behaviour The `ws: true` flag on the proxy entry tells Vite (which uses `http-proxy-middleware`) to forward the upgrade request. Cookies attached to the upgrade carry through transparently — that's the whole point of running same-origin in dev. One gotcha: the proxy's `target` for a WS route must use a `ws://` or `wss://` scheme, _not_ `http://`. If you accidentally use `http://`, the upgrade fails silently and the SPA gets a connection error. Document in the README. ## Acceptance criteria - [ ] `pnpm dev` starts at `http://localhost:5173`; the SPA loads. - [ ] With Directus running locally on `:8055`, `fetch('/api/server/info')` from the browser console returns Directus's server info — proxy is forwarding. - [ ] WebSocket smoke test: `new WebSocket('ws://localhost:5173/ws-business')` connects (returns code 1006 if Directus's WS endpoint is locked down without auth, but the upgrade itself succeeds — that's the test). - [ ] `import { foo } from '@/auth/foo'` resolves both at type-check time and at runtime. - [ ] `pnpm typecheck` is stricter — adding `arr[0].whatever` without a check should now fail. - [ ] No CORS errors in the browser console when calling `/api/...` from `http://localhost:5173`. ## Risks / open questions - **Directus's WS path is `/websocket`, not `/ws`.** Confirmed by reading Directus 11.x source. The proxy `rewrite` rule reflects this. - **Processor WS path is unsettled.** [[processor-ws-contract]] uses `/processor/ws` as illustrative; the actual path will be set by the proxy config in stage/prod (`trm/deploy`). For dev we assume the Processor binds to `:8081` with no path prefix, and the proxy adds the `/ws-live` prefix on the SPA side. Reconcile if Phase 1.5 lands with a different path convention. - **Multiple Vite apps on one machine.** Pinning to `:5173` collides if another Vite app is running. Document the override (`VITE_PORT=5174 pnpm dev` works as a one-off; permanent change needs the config edit). ## Done `vite.config.ts` switched to the function form, reads env via `loadEnv(mode, process.cwd(), 'VITE_')`, exposes three proxy routes: - `/api/*` → `${VITE_DEV_DIRECTUS_URL}/*` (default `http://localhost:8055`), strips the `/api` prefix. - `/ws-business` → derived `ws://...` from the Directus URL, rewrites to `/websocket`. - `/ws-live` → `${VITE_DEV_PROCESSOR_WS_URL}` (default `ws://localhost:8081`), strips the `/ws-live` prefix. `tsconfig.app.json` gains `noUncheckedIndexedAccess: true` and `noImplicitOverride: true`. The `@/*` path alias was already set up in 1.2 (deviation noted there). `.env.example` (committed) documents the two proxy override env vars; `.env.local` (already gitignored via the existing `*.local` pattern) is the developer's actual values file. README "Local dev" section rewritten with the proxy table and override instructions. **Deviation from spec:** the spec called for `.env.dev.example` / `.env.dev.local` but Vite's mode for `pnpm dev` is `development` (not `dev`), so mode-specific files would be `.env.development.local` and Vite wouldn't auto-load `.env.dev.local` at all. Switched to the standard Vite convention `.env.example` (committed docs) + `.env.local` (auto-loaded in any mode, gitignored). Cleaner and matches Vite's actual behaviour. **Smoke check:** `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm build` all green. Stricter tsconfig didn't surface any existing issues (current code is small). Landed in `PENDING_SHA`.