A modern Single Page Application (SPA) built with Svelte 5 and SvelteKit, designed as the frontend for the Booqr booking management system. This application emphasizes security, accessibility, and standards-first development with a minimalist, composable architecture.
- Lean Dependency Tree: Tree-shaken, code-split chunks — no monolithic "vendor" bundle.
- Svelte 5 with Runes: Modern reactive programming using Svelte 5's runes
system (
$state,$derived,$effect,$props) for efficient state management. - SvelteKit SPA Mode: Static site generation targeting static deployments with client-side routing.
- Internationalization: Paraglide JS provides English and Danish messages, browser-language detection, and a persisted language preference.
- Tailwind CSS: Utility-first CSS framework for consistent, minimal styling.
- OpenAPI Code Generation: Auto-generated API client from OpenAPI specification.
- TanStack Query: Declarative data fetching and
caching via route-local
*Data.svelte.jshooks, with automatic 401 refresh-and-retry and coarse cache invalidation. - Multi-tenancy: Each
*.booqr.dksubdomain resolves its tenant from the API at runtime; unknown subdomains redirect to the marketing site; reserved apex hosts are excluded synchronously. - JWT Authentication: Secure authentication flow.
- ES2022 Target: Modern JavaScript features for optimal performance.
- Semantic HTML5: Strict adherence to semantic markup and WCAG AA accessibility standards.
- Security-First Design: OWASP-aligned security practices.
- Playwright E2E Testing: End-to-end testing with semantic selectors and accessibility assertions.
- Container Security: Runs rootless in minimal Alpine Linux images (~20MB) with
immutable filesystem.
- Lighttpd Web Server: Secure, minimal-attack-surface static web server in production.
- Two-Stage Docker Builds: Reproducible builds with separate build and runtime stages for optimal security and size.
The application is organized following SvelteKit's file-based routing conventions:
- src/routes: Page components and routes
- src/lib: Shared utilities and components
- src/routes/+layout.svelte: Top-level layout with semantic navigation
- e2e: Playwright end-to-end tests
- static: Static assets served at root
- Dockerfile: Multi-stage build configuration
- lighttpd.conf: Production web server configuration
- AGENTS.md: Development guidelines for AI agents
Note: Never manually edit files in src/lib/api/ as they are auto-generated.
The app uses Paraglide JS for all interface text. Source messages live in messages/en.json and messages/da.json;
the Vite plugin compiles them into src/lib/paraglide/. Import translated strings as m from
$lib/paraglide/messages.js, for example m.navSignIn(). Do not edit the generated Paraglide output directly.
Paraglide resolves the locale in this order: its localStorage preference, the browser's preferred language, then
English. The footer language control uses src/lib/locale.svelte.js to call Paraglide's setLocale(). It intentionally
reloads the SPA so every translated module initializes with the new locale.
src/routes/+layout.svelte is the document-level integration point. It sets <html lang> and text direction from
getLocale() / getTextDirection() inside a component $effect, while also using translated labels in shared layout
UI. This keeps the document language correct for assistive technology as well as visual translations.
The SPA is served for every *.booqr.dk subdomain. Tenant identity is resolved at runtime from the host; the API is
the sole authority — never assume a subdomain is valid client-side.
src/lib/tenant.svelte.js exports a tenant singleton (TenantState runes class) holding { displayName, slug }
and a status field (loading → resolved | notFound | reserved | error). hostCategory() synchronously
classifies the current host as 'reserved' (the apex booqr.dk, www, status, mta-sts) or 'tenant' (everything else,
including subdomains, localhost, and preview hosts). Reserved hosts skip the API fetch entirely.
The root layout (src/routes/+layout.svelte) bootstraps tenant resolution client-side: a $effect calls
GET /api/my-tenant. A 200 response resolves branding; a typed 404 (tenant-not-found ProblemDetails) triggers
a full-page redirect to MARKETING_URL; any other failure (bare 404, 500, network error) sets tenant.isError so
the user can retry. The bootstrap IIFE is fire-and-forget — errors must not be re-thrown.
Per-tenant branding flows from tenant.displayName into the NavBar and <title>. The Paraglide message
m.marketingHeading() is the fallback — never hardcode 'Booqr'.
A 403 whose ProblemDetails type ends in /problems/tenant-mismatch is detected by isTenantMismatch() in
src/lib/queryClient.js. It clears the session and cache then redirects to /login. This is distinct from the 401
refresh-and-retry path — a tenant-mismatch cannot be fixed by refreshing the token.
Copyright (C) 2026 Mads Klinkby (https://www.kli.dk)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/.