Skip to content

Latest commit

 

History

244 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Klinkby.Booqr.App

Docker Image CI CodeQL License

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.

Features

  • 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.js hooks, with automatic 401 refresh-and-retry and coarse cache invalidation.
  • Multi-tenancy: Each *.booqr.dk subdomain 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.

Project Structure

The application is organized following SvelteKit's file-based routing conventions:

Note: Never manually edit files in src/lib/api/ as they are auto-generated.

Internationalization

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.

Multi-tenancy

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 (loadingresolved | 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.

Licensed under AGPL-3.0

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

About

Frontend for Booqr.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages