The Svelte frontend (front/)¶
Svelte 5 SPA built with SvelteKit's adapter-static. Talks to the
backend over /api/v1/ and to the SFU over /ws. Bring your own
UI if you prefer — the frontend is one product, not the product.
Stack¶
| Layer | Choice |
|---|---|
| Framework | Svelte 5 (runes: $state, $derived, $effect) |
| Meta-framework | SvelteKit (SPA mode via @sveltejs/adapter-static) |
| Build | Vite 6 |
| TypeScript | strict, via svelte-check |
| Styling | plain scoped <style> blocks (no Tailwind, no Panda) |
| i18n | i18next (4 languages × 10 namespaces) |
| Testing (unit) | Vitest 4 + @testing-library/svelte + jsdom |
| Testing (e2e) | Playwright (headless Chromium) |
| WebRTC protocol | Galene's protocol.js (vendored, unchanged) |
Module map¶
front/src/
├── app.html
├── lib/
│ ├── api/ ← typed HTTP client for /api/v1/
│ │ ├── fetchApi.ts ← fetch wrapper + ApiError
│ │ ├── auth.ts ← login, logout, fetchMe
│ │ ├── config.ts ← public config + resolveGaleneWsUrl
│ │ ├── events.ts ← joinEvent, fetchParticipants, …
│ │ ├── rooms.ts ← createEphemeralRoom
│ │ └── schema.d.ts ← generated from openapi.json
│ ├── components/ ← app-specific components (PreJoin, ChatPanel, VideoTile, …)
│ ├── primitives/ ← reusable UI (Button, Input, Field, Select, …)
│ ├── layout/ ← shell (Header, Footer, Toast, …)
│ ├── protocol/ ← Galene's WebSocket protocol (vendored)
│ ├── stores/ ← reactive state (21 .svelte.ts modules)
│ │ ├── connection.svelte.ts ← ServerConnection wrapper, 99% covered
│ │ ├── session.svelte.ts ← login/logout/hydrate
│ │ ├── chat.svelte.ts ← message queue
│ │ ├── media.svelte.ts ← device enumeration, local/remote streams
│ │ ├── notifications.svelte.ts ← toast queue
│ │ └── …
│ ├── i18n/ ← i18next bootstrap + locale JSON
│ └── utils/
├── routes/
│ ├── +layout.svelte ← app shell
│ ├── +page.svelte ← home (New meeting button)
│ ├── auth/login/ ← login page
│ └── m/[slug]/ ← meeting route (PreJoin → Conference)
└── styles/
└── global.css
State management¶
No Svelte stores in the classic sense — the app uses
.svelte.ts modules with Svelte 5 runes:
export const connectionState = $state({
state: 'disconnected' as 'disconnected' | 'connecting' | 'connected' | 'joined',
users: {} as Record<string, User>,
permissions: [] as string[],
// …
});
Each store exports its state object directly. Components import it and read fields as normal object properties — Svelte 5's reactivity does the tracking.
Testing¶
cd front
npm run test # Vitest (157 pass)
npm run coverage # with v8 coverage report
npx playwright test # e2e (2 pass: single-guest + two-guest-with-chat)
From the repo root:
make test-front # Vitest only
make e2e-all # full stack + Playwright
make e2e-alt # same, but against alt-backend
Line coverage on src/lib/ is ~21%, heavily weighted toward
api/ (100%), utils/ (91%), and a handful of stores
(connection.svelte.ts at 99%, notifications.svelte.ts at 97%).
Components and primitives are mostly at 0% — the next push target
is render tests for PreJoin, VideoTile, and ChatPanel.
Type generation¶
The TypeScript types for /api/v1/ come from the backend's
OpenAPI spec via openapi-typescript:
# Refresh after the backend's OpenAPI changes
cd backend && uv run python -c "from app.app import create_app; ..." > ../front/openapi.json
cd ../front && npm run generate:api
front/openapi.json + front/src/lib/api/schema.d.ts are
committed to the repo — regenerating needs the backend running,
which is friction for frontend-only changes. A CI guard that
re-generates and diffs is on the roadmap.
Dev proxy¶
vite.config.ts proxies from the dev server (:5173) to:
| From | To | Why |
|---|---|---|
/api/* |
http://localhost:8000 |
Backend (Litestar or alt-backend) |
/ws (WebSocket) |
http://localhost:8443 |
SFU signalling |
/galene-api |
http://localhost:8443 |
(legacy path kept for dev-only back-compat — not used in v2 flow) |
Further reading¶
notes/v2/frontend.md— design doc- API contracts —
/api/v1/shape front/README.md— operational notes