Files

8.4 KiB

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:
      • /apihttp://localhost:8055 (Directus REST/GraphQL).
      • /ws-businessws://localhost:8055/websocket (Directus business-plane WS), with ws: true and rewrite to strip /ws-business.
      • /ws-livews://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: <exact-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

// 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:

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 39b60c9.