- auth.users: new nullable `onboarding_completed_at` column - new /api/v1/me/onboarding routes: GET, POST /complete, PATCH /reset - onboardingStatus Svelte store in the web app that reads/writes via those endpoints (no JWT claim so completing the flow takes effect without a token re-mint) - docs/plans/onboarding-flow.md adjusted: no backfill (launch without existing users), better-auth `name` clarified, 7 templates including "Arbeit" confirmed Foundation for the 3-screen first-login flow (Name → Look → Templates). No UI and no route guard yet — those ship in M2 when the redirect target actually exists. Schema change is a pure column-add, applied via `pnpm --filter @mana/auth db:push`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
Onboarding Flow
Status: proposed Scope: UX + auth + workbench data-layer Owner: till Created: 2026-04-23
Problem
Mana launcht bald mit 27+ Modulen in einer einzigen App. Ein Erstnutzer
landet heute nach Signup auf /welcome (localStorage-Flag
HAS_SEEN_WELCOME), sieht eine generische Intro, und wird dann in eine
hart kodierte Home-Scene mit todo/calendar/notes fallen gelassen.
Effekte:
- Nichts fragt, wer der Nutzer ist — jede Greeting bleibt anonym.
- Nichts fragt, was er mit Mana tun will — wer für Fitness-Tracking kam, sieht Work-Defaults und bounct; wer für Journaling kam, findet das Modul nur über die Gallery.
- Nichts macht die App visuell „seins" — die 8 vorhandenen Theme-Varianten sind ein starkes Differenzierungs-Feature, das erst in Settings zu finden ist.
Goals
- 3-Screen-Onboarding nach Signup: Name → Look → Templates.
displayNamedauerhaft in mana-auth persistieren, damit UI und Sync-Jobs einen Handle haben.- Nutzer wählt ein Theme aus den 8 vorhandenen Varianten als erstes „make it yours"-Moment.
- Nutzer wählt 1+ Use-Case-Templates (Health, Lernen, Sport, Entdecken, Erinnern, Alltag); deren Union bestimmt, welche Module als Cards in der Default-Home-Scene gepinnt werden.
- Jederzeit überspringbar (pro Screen + „alles überspringen"), mit sinnvollen Fallbacks.
Non-goals
- Kein Space-Type-Picker im Flow. Nutzer starten im automatisch angelegten Personal-Space; Family/Team/Brand/Club/Practice bleiben hinter dem existierenden Space-Creation-Flow — v2-Thema.
- Kein Avatar-Upload. Me-Images hat seinen eigenen Flow
(
/profile/me-images) und würde das Onboarding sprengen. - Kein Per-Modul-Onboarding. Module wie
newshaben eigeneonboardingCompleted-Flags und bleiben orthogonal. - Keine dynamische/LLM-generierte Template-Empfehlung in v1 — statischer Katalog ist inspectable, testbar, in einer Woche shipbar.
Current state
- Signup →
/welcome(apps/mana/apps/web/src/routes/welcome/+page.svelte:19).HAS_SEEN_WELCOMEim localStorage: client-only, nicht cross-device, nicht auto-disabled nach einmaligem Sehen. - AuthUser-Shape: Better-Auth nutzt
user.name(nichtdisplayName) als primäres Feld (services/mana-auth/src/db/schema/auth.ts:40); Client-sideUserData.name(packages/shared-auth/src/types/index.ts:75) spiegelt das wider, ist aber optional weil nicht jeder Flow ihn gesetzt hat. - SpaceTypes:
personal | brand | club | family | team | practice(packages/shared-types/src/spaces.ts:29). Personal-Space wird via Sentinel_personal:<userId>automatisch angelegt (apps/mana/apps/web/src/lib/data/scope/bootstrap.ts:24). - Theme-Store:
createThemeStore({appId:'mana', defaultVariant:'ocean'})(apps/mana/apps/web/src/lib/stores/theme.ts:18). 8 Varianten:lume | nature | stone | ocean | sunset | midnight | rose | lavender. Settings synced überGlobalSettings.thememitmana-auth. - Home-Scene-Default: hart kodiert
(
apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts:47):const DEFAULT_HOME_APPS = [{appId:'todo'},{appId:'calendar'},{appId:'notes'}]; workbenchScenesStore.createScene()existiert schon und ist deterministisch genug, um aus dem Onboarding heraus eine alternative Start-Scene zu schreiben.- App-Registry:
apps/mana/apps/web/src/lib/app-registry/apps.ts+packages/shared-branding/src/mana-apps.tshalten 27+ Module mit Icons, Tiers und Kurzbeschreibungen.
Design
Flow
signUp → /onboarding/name → /onboarding/look → /onboarding/templates → /
Route-Guard im Root-Layout: authentifizierter User + onboardingCompletedAt === null
→ redirect auf /onboarding/name, außer man ist bereits auf einer
/onboarding/*-Route.
Screen 1 — Name
- Headline: „Wie willst du genannt werden?"
- Text-Input (1–40 chars, trim), Placeholder greifbar („z. B. Till")
- „Weiter"-Button disabled bei leerem Wert
- „Überspringen" oben rechts → fallback
name = email.split('@')[0] - Submit: POST
mana-auth→ aktualisiert Better-Authuser.name(z. B. überauthClient.updateUser({ name })oder einen eigenen Endpoint) - Keep simple: Enter = Weiter
Screen 2 — Look
- Headline: „Hi {displayName}, wähle deinen Look"
- 8 Theme-Tiles mit Live-Preview (Mini-Workbench-Stack in dem Variant)
- Oben: Mode-Toggle
Hell | Dunkel | Systemals separate Row - Single-Select, Klick setzt direkt:
theme.setVariant(variant)(live, lokal)userSettings.updateGlobal({ theme: { mode, colorScheme } })(persistent)
- „Weiter" immer aktiv (Default vorselektiert: aktueller Wert oder
ocean)
Screen 3 — Templates
-
Headline: „Wofür willst du Mana nutzen?"
-
7 Tiles (Multi-Select, Icon + Name + 1-Liner):
Template Module-Vorschlag (Reihenfolge = Priorität) Alltag todo,calendar,notes,contactsArbeit todo,calendar,mail,chat,times,notesHealth habits,body,mood,food,periodSport habits,body,food,goals,stretchLernen skilltree,quiz,notes,library,kontextEntdecken places,citycorners,photos,music,wetterErinnern memoro,journal,photos,moodlit,quotesAlle Modul-IDs gegen
apps/mana/apps/web/src/lib/app-registry/apps.tsverifiziert (2026-04-23). Dedup über Templates hinweg passiert im Finish-Handler; Prioritäts-Reihenfolge bleibt erhalten. -
„Fertig"-Button:
- Union der
moduleIdsdeduplicaten, Reihenfolge = erstes Vorkommen in Template-Priorität - Cap bei 8 Modulen (2×4 Grid) — Rest bleibt über „App hinzufügen" erreichbar
workbenchScenesStore.createScene({ name: 'Zuhause', apps })— überschreibtDEFAULT_HOME_APPSfür diesen User- PATCH
onboardingCompletedAt = now() - Redirect
/
- Union der
-
„Überspringen": Scene wird nicht erstellt; fällt zurück auf
DEFAULT_HOME_APPS.onboardingCompletedAtwird trotzdem gesetzt (sonst Flow-Schleife).
Data changes
mana-auth: neues Feld onboardingCompletedAt: Date | null auf
auth.users-Tabelle (nullable, default null). Kein Backfill nötig —
Launch ohne bestehende Nutzer.
Migration: einfache Drizzle-Schema-Erweiterung. pnpm --filter @mana/auth db:push wendet den Column-Add an, keine hand-authored SQL nötig.
Read-Path: dedizierter Endpoint
GET /api/v1/me/onboarding → { completedAt: Date | null } statt JWT-
Claim, damit wir nach POST /complete nicht den Token neu minten
müssen. Client-seitiger Store lädt bei Auth-Ready einmalig.
Write-Path:
POST /api/v1/me/onboarding/complete→ setztonboardingCompletedAt = now(), idempotent (kein Update wenn bereits gesetzt)PATCH /api/v1/me/onboarding/reset→ setzt aufnull, für das Settings-Re-Trigger in M5
Shared-branding: neue Datei
packages/shared-branding/src/onboarding-templates.ts:
export type OnboardingTemplateId = 'alltag' | 'arbeit' | 'health' | 'sport' | 'lernen' | 'entdecken' | 'erinnern';
export type OnboardingTemplate = {
id: OnboardingTemplateId;
name: string; // DE
shortDescription: string; // 1-Liner
icon: IconName; // aus @mana/shared-icons
moduleIds: string[]; // max 5, Reihenfolge = Priorität
};
export const ONBOARDING_TEMPLATES: readonly OnboardingTemplate[] = [...];
Warum shared-branding, nicht webapp-lokal: die Template-Definition
ist Branding/Produkt-Entscheidung, nicht UI-Logik — memoro (mobile)
kann sie später mit-konsumieren.
Route-Struktur
apps/mana/apps/web/src/routes/onboarding/
├── +layout.svelte # Progress-Dots (3), „alles überspringen"
├── +layout.ts # Guard: onboardingCompletedAt gesetzt → redirect /
├── name/+page.svelte
├── look/+page.svelte
└── templates/+page.svelte
Reuse, don't rebuild
- Theme-Picker: Die Tiles aus
/themesextrahieren inThemeSelector.svelte, in beiden Screens mounten. - Module-Tiles nutzen
APP_ICONSauspackages/shared-branding/src/app-icons.ts+mana-apps.tsfür Namen/Beschreibung — kein Duplikat. - Scene-Creation nutzt
workbenchScenesStore.createScene()— kein neues Data-Layer-Primitiv. - Settings-Sync:
userSettings.updateGlobal()existiert schon und synct über mana-auth.
Implementation steps
M1 — Data model + Backend + Client-Store (~1 Tag)
- Drizzle:
onboardingCompletedAtcolumn aufauth.users - Endpoints: GET/POST/PATCH unter
/api/v1/me/onboarding/* - Client-Store:
$lib/stores/onboarding-status.svelte.tsmitload()/markComplete()/reset() - Kein Route-Guard, kein UI — Plumbing only, kann ohne Feature-Flag mergen. Guard kommt in M2 wenn Screens existieren (sonst würde Redirect auf 404 zeigen).
M2 — Route-Guard + Shell + Screen 1 (Name) (~0.5 Tag)
- Guard in
(app)/+layout.svelte→goto('/onboarding/name')wennonboardingStatus.completedAt === nullund Pfad nicht schon/onboarding/* /onboarding/+layout.sveltemit Progress-Dots + Skip-All, Haupt- Chrome (PillNav/Bottom-Stack) ausblenden für clean UI/onboarding/name/+page.svelte+authClient.updateUser({ name })- E2E: Signup → Name-Screen → Weiter →
/onboarding/look
M3 — Screen 2 (Look) (~0.5 Tag)
ThemeSelector.svelteaus/themesextrahieren/onboarding/look/+page.svelte, Persistenz viauserSettings.updateGlobal({theme})- E2E: Theme-Klick → CSS-Variablen wechseln live → reload → Theme bleibt
M4 — Templates + Screen 3 (~1 Tag)
ONBOARDING_TEMPLATESdefinieren, Modul-IDs gegen Registry verifizieren (siehe Open Question 1)/onboarding/templates/+page.sveltemit Multi-Select-Tiles- Finish-Handler: dedupliziertes Modul-Set →
createScene(),onboardingCompletedAt = now(), Redirect/ - E2E: Health + Sport picken → Home-Scene enthält
habits, body, food, mood, period, goals(dedupliziert, max 8)
M5 — Polish + Re-Trigger (~0.5 Tag)
- Settings → Account → „Onboarding erneut durchlaufen" setzt
onboardingCompletedAt = null /welcome-Seite: weiter als Marketing-Landing behalten oder deprecaten (TBD, offene Frage)- Analytics-Events:
onboarding_screen_viewed,onboarding_template_picked,onboarding_completed,onboarding_skipped_at_{name|look|templates}
Total: ~3.5 Tage bei realistischer Schätzung. Parallelisierbar zu zweit auf ~2 Tage (M1 einer, M2+M3 parallel, M4 sobald M1 durch).
Tradeoffs
- 3 Screens ist am oberen Ende. Jeder Screen ist ein Drop-off-Risk. Kompensiert durch Skip pro Screen + Defaults. Schnellster Path ist 3× Enter.
- Multi-Select vs. Exklusiv bei Templates: Multi-Select gewinnt, weil Health+Sport oder Lernen+Alltag echte reale Kombis sind. Dedup ist billig, keine UX-Kosten.
- Statisch vs. LLM-generiert: Statisch ist inspectable, testbar, Fehler-armer erster Eindruck. LLM-Variante ist v2.
- Theme in Onboarding vs. Settings-only: Risiko, dass Nutzer
overwhelmed sind von 8 Varianten bei Signup. Mitigation: ein
sinnvolles Default (
ocean) ist vorselektiert, „Weiter" ohne Klick ist OK. - Templates in
shared-branding: minimaler Lock-in für memoro (mobile). Alternative: web-lokal bis jemand mobile-Onboarding baut.shared-brandinggewinnt — Definition ist Produkt, nicht UI. - Route-Guard im Root-Layout vs. Hook in jedem Module: Root-Layout zentralisiert Redirect-Logik, eine Stelle zum Ändern. Hook pro Modul wäre defensiver, aber N-mal Duplicate. Root gewinnt.
Rollout
- Feature-Flag
ONBOARDING_V1_ENABLED(default off in prod) - M1–M4 Bundle mergen, dann Flag auf Staging flippen, smoketesten
- Bestehende User sind durch Backfill geschützt — nur Neu-Signups nach Flag-on sehen den Flow
- Drop-off-Monitoring pro Screen; falls > 30% an einer Stelle → Screen-Inhalt kürzen oder Skip prominenter machen
- Settings-Re-Trigger (M5) lässt Early-Adopter freiwillig durchlaufen
- Live-Flip nach ~3 Tagen Staging-Soak ohne kritische Findings
Resolved decisions (2026-04-23)
- Modul-IDs verifiziert gegen
apps.ts. Alle 29 in Templates referenzierten IDs existieren:todo, calendar, notes, contacts, mail, chat, times, habits, body, mood, food, period, goals, stretch, skilltree, quiz, library, kontext, places, citycorners, photos, music, wetter, memoro, journal, moodlit, quotes. - „Arbeit" als eigenes Template aufgenommen (7 Templates total). Überlappung mit „Alltag" (todo/calendar/notes) ist gewollt und wird dedupliziert — „Arbeit" ergänzt Mail/Chat/Times, „Alltag" ergänzt Contacts.
- Kein Tier-Gating im Onboarding. Templates zeigen alle Module
unabhängig von
requiredTier. Wenn Production-Tiers reaktiviert werden, greift das normaleAuthGatebeim Öffnen — das Modul-Tile zeigt sich, nur das Öffnen führt ggf. zu Upgrade-Prompt. Bewusste Entscheidung, Discovery nicht zu gaten. - Max-Module-Cap bleibt bei 8 (2×4 Grid). Bei 7 gewählten Templates mit Dedup landen ~25 Unique-Module im Pool — die ersten 8 nach Template-Prioritäts-Reihenfolge gewinnen, Rest über „App hinzufügen" erreichbar.
Still open
- Welcome-Page behalten oder löschen? Als Marketing-Landing für Logged-out-Pre-Signup könnte sie bleiben. Entscheidung in M5.
- Mobile (memoro)? Onboarding ist v1 web-only; memoro könnte
onboardingCompletedAt+ Templates künftig mit-konsumieren. Nicht blockierend. - Default-Mode Hell/Dunkel/System? Aktueller Default ist unklar —
muss ich mit dem aktuellen Verhalten von
createThemeStoreabgleichen, bevor Screen 2 fest verdrahtet ist. Wird in M3 geklärt (ein-Zeiler-Lookup).