Training Tracker als Progressive Web App

Digitalisierung unserer Trainingsteilnahmeliste
Tracking von Trainingsanwesenheit mit einer selbstgebauten PWA
Ein Erfahrungsbericht über die Entwicklung einer Progressive Web App
Use Case: Erfassung der Teilnahme an Radsport-Trainings — mit Geo-Fencing, Offline-Fallback und einer Architektur, die sowohl auf dem Smartphone als auch im Browser läuft.
1. Die Idee: Was sollte gebaut werden?
In unserem Radsportverein trainieren wir in mehreren Gruppen — Roadbike, Mountainbike, Gravelbike — an verschiedenen Wochentagen und Treffpunkten. Bisher lief die Anwesenheitserfassung per Liste die vor Ort lag und jeder der zum Training kommt macht mit dem Edding ein Kreuz am Tag des Trainings. Das ist unzuverlässig, nicht nachvollziehbar und vor allem nicht digital. Es kam auch schon vor, dass der Plan verschwunden war.
Die Liste ist ein kleiner Trainingsansporn, denn am Ende des Jahres bekommt der Trainingsbeste einen kleinen Pokal ;-)
Die Anforderung: Ein digitaler Check-in, der sicherstellt, dass Mitglieder tatsächlich vor Ort sind. Kein Einloggen von zu Hause. Dazu sollte der Check-in nur in einem bestimmten Zeitfenster möglich sein — zu früh oder zu spät geht nicht.
Das Ergebnis ist eine Progressive Web App (PWA), die:
- den Standort des Smartphones mit dem vereinbarten Treffpunkt abgleicht
- den Check-in auf ein Zeitfenster von ±30 Minuten um den Trainingsstart begrenzt
- auf dem Smartphone installiert werden kann und sich anfühlt wie eine native App
- Trainings-Sessions verwaltet und Teilnahmelisten in Echtzeit anzeigt
- Admins erlaubt, Sessions und Benutzer zu verwalten
2. Die Architektur: Ein Überblick
Die App besteht aus drei Schichten, die klar voneinander getrennt sind:
- Frontend: Svelte 5 + SvelteKit 2
- SPA (Single Page Application)
- PWA mit Service Worker
- Carbon Design System (IBM)
API Layer: SvelteKit Server Routes & Deno als Runtime
- REST-API für User, Sessions, Attendance
- Validierung auf Serverseite
Persistierung: CouchDB (IBM Cloudant oder lokal)
- Dokumentenorientiert (JSON)
- Kein Schema-Zwang
- (frei) verfügbar als Cloudant Service in der IBM Cloud
Frontend: Svelte 5 mit SvelteKit
Die Wahl fiel auf Svelte 5 mit SvelteKit 2 — ein modernes Full-Stack-Framework, das sowohl Client- als auch Server-Code in einem Projekt vereint. Die Besonderheit: Svelte arbeitet zur Compile-Zeit, nicht zur Runtime. Das bedeutet weniger JavaScript im Browser und schnellere Ladezeiten.
Die UI verwendet IBMs Carbon Design System — eine etablierte Komponentenbibliothek, die konsistente, barrierefreie Oberflächen liefert.
PWA mit Service Worker
Ein Service Worker sorgt dafür, dass die App offline-fähig ist:
- Statische Assets (HTML, CSS, JS): Cache-First — die App lädt sofort aus dem Cache
- API-Aufrufe (
/api/*): Network-First — falls das Netzwerk nicht verfügbar ist, wird auf den Cache zurückgegriffen
Das Web-App-Manifest definiert display: standalone, sodass die App nach Installation
auf dem Smartphone in einem eigenen Fenster ohne Browser-UI läuft.
Serverseite: SvelteKit API Routes
Die API-Endpunkte sind als SvelteKit +server.ts-Dateien implementiert — pro Ressource
eine Datei mit HTTP-Methoden als exportierte Funktionen. Die Authentifizierung erfolgt
über einfache Login-Name/Password-Kombination.
Datenbank: CouchDB
CouchDB ist eine dokumentenorientierte NoSQL-Datenbank, die Daten als JSON speichert. Die Wahl fiel auf CouchDB aus mehreren Gründen:
- Einfache Containerisierung möglich: Ein offizielles Docker-Image, Konfiguration über Umgebungsvariablen
- Nutzung IBM's Service - Cloudant o. lokale CouchDB im Dockercontainer
- Kein Schema-Zwang: Datenmodelle können sich leicht ändern, ohne Migrationen
- HTTP-API: Direkt über REST erreichbar, kein ORM nötig
- Drei Datenbanken:
users,sessions,attendance— jede logisch getrennt
Architektur-Pattern: Clean Architecture
Die Geschäftslogik ist strikt von der Infrastruktur getrennt:
src/
├── domain/ # Entitäten & Interfaces (keine Abhängigkeiten)
│ ├── models/ # User, TrainingSession, AttendanceRecord
│ └── services/ # IUserRepository, IAttendanceRepository (Contracts)
├── application/ # Use Cases (CheckIn, SignUp, etc.)
├── infrastructure/ # Implementierungen (CouchDB, LocalStorage, Geolocation)
├── presentation/ # Svelte-Komponenten
└── routes/ # API-Endpunkte + SeitenDieses Muster ermöglicht es, die Datenhaltung zu wechseln, ohne die Geschäftslogik anzurühren. Tatsächlich gibt es zwei Implementierungen der Repositories: eine für CouchDB und eine für den LocalStorage des Browsers — letztere dient als Fallback für die Entwicklung ohne laufende Datenbank.
3. Entwicklungsansatz: PWA-First, dann Deployment
Phase 1: Domain-Modell und Use Cases
Begonnen wurde mit den Entitäten — was sind User, Sessions, AttendanceRecords? Dann folgten die Use Cases als reine TypeScript-Klassen ohne Framework-Abhängigkeiten. So konnte die Logik früh getestet werden, ohne dass eine Datenbank oder ein Browser nötig war.
Phase 2: UI als Single-Page App
Die gesamte Anwendung läuft auf einer Seite (+page.svelte). Tab-Navigation
wechselt zwischen Training, Profil, Summary und Admin. Das vereinfacht den PWA-Zustand
— die App hat genau eine URL, kein Routing innerhalb der App nötig.
Phase 3: Server-Routes und CouchDB
Parallel zur UI entstanden die REST-Endpunkte. Die Server-Routes greifen direkt auf CouchDB zu — kein ORM, keine Abstraktionsschicht zwischen SvelteKit und der DB.
Phase 4: Docker-Container
Für das Deployment gibt es einen Multi-Stage-Build:
- Builder-Stage: Node 22 Alpine, npm ci,
npm run build - Runner-Stage: Nur das Build-Artifact + node_modules, non-root User
Docker Compose orchestriert zwei Container: die SvelteKit-App und CouchDB. Die Verbindung läuft über das interne Docker-Netzwerk, CouchDB hat einen Healthcheck, und die App startet erst, wenn die Datenbank bereit ist.
4. Deployment und Betrieb
Die Compose-Datei ist bewusst einfach gehalten:
services:
couchdb:
image: couchdb:3
volumes:
- couchdb-data:/opt/couchdb/data
app:
image: training-app:latest
depends_on:
couchdb:
condition: service_healthyZur Laufzeit konfiguriert die App über Umgebungsvariablen (COUCHDB_ENDPOINT,
COUCHDB_USER, COUCHDB_PASSWORD) die Verbindung zur Datenbank.
Ein Seed-Script (npm run seed) legt Beispiel-Daten an.
5. Ergebnis
5. Erfahrungen und Learnings
Was gut funktioniert hat:
- SvelteKit als Full-Stack-Framework: Ein Projekt, eine Sprache, ein Build-System für Client und Server — reduziert die Komplexität enorm.
- Clean Architecture: um zusätzlich eine LocalStorage-Variante zu bauen, musste kein Use Case verändert werden — nur ein neues Repository implementiert und in der DI ausgetauscht.
- CouchDB als Docker-Container: Installation, Konfiguration und Backup — alles über Docker-Volumes und Umgebungsvariablen.
- PWA ohne Framework-Overhead: Der Service Worker hat rund 60 Zeilen Code.
Kein Workbox, kein Workaround — einfach
fetch-Event-Handler.
Was man beim nächsten Mal anders machen würde:
- Kein VITE_-Präfix für Config-Variablen: Wir haben
VITE_USE_COUCHDBverwendet, was zur Build-Zeit eingebrannt wird. Besser: Serverseitige Config, die über$env/dynamic/privatezur Laufzeit gelesen wird. Mussten wir nachträglich umbauen. - CouchDB-Index für Attendance-Queries: Ohne vorher definierte Indizes können
Mango-Queries auf
dateundsessionIdbei vielen Daten langsam werden. - HTTPS von Anfang an: Die Geolocation API setzt einen sicheren Kontext voraus. Wer PWA mit Standortermittlung baut, kommt an HTTPS nicht vorbei.
6. Fazit
Die App löst ein konkretes Problem im Vereinsalltag — und das mit bemerkenswert schlankem Technologie-Stack. SvelteKit + CouchDB + Docker ist eine Kombination, die sich vor allem für kleine bis mittlere Webanwendungen mit klaren Datenstrukturen eignet. Kein Microservice-Overkill, kein Cloud-Bindungseffekt, keine überdimensionierte Architektur.
Für IT-Manager: Das Projekt zeigt, dass eine moderne Webanwendung mit PWA-Features und standortbasierter Interaktion mit einem Team von einer Person und wenigen Wochen realisierbar ist — vorausgesetzt, Architektur und Tech-Stack sind sauber gewählt.
Für Entwickler: Ein gutes Beispiel, wie Clean Architecture in der Praxis aussieht — nicht als dogmatisches Pattern, sondern als pragmatische Entscheidung für Wartbarkeit und Austauschbarkeit.