mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
- 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>
320 lines
14 KiB
Markdown
320 lines
14 KiB
Markdown
# 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.
|
||
- `displayName` dauerhaft 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 `news` haben eigene
|
||
`onboardingCompleted`-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_WELCOME` im localStorage: client-only, nicht cross-device,
|
||
nicht auto-disabled nach einmaligem Sehen.
|
||
- **AuthUser-Shape**: Better-Auth nutzt `user.name` (nicht
|
||
`displayName`) als primäres Feld (`services/mana-auth/src/db/schema/auth.ts:40`);
|
||
Client-side `UserData.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 über `GlobalSettings.theme` mit `mana-auth`.
|
||
- **Home-Scene-Default**: hart kodiert
|
||
(`apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts:47`):
|
||
```ts
|
||
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.ts` halten 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-Auth `user.name` (z. B.
|
||
über `authClient.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 | System` als 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`, `contacts` |
|
||
| **Arbeit** | `todo`, `calendar`, `mail`, `chat`, `times`, `notes` |
|
||
| **Health** | `habits`, `body`, `mood`, `food`, `period` |
|
||
| **Sport** | `habits`, `body`, `food`, `goals`, `stretch` |
|
||
| **Lernen** | `skilltree`, `quiz`, `notes`, `library`, `kontext` |
|
||
| **Entdecken** | `places`, `citycorners`, `photos`, `music`, `wetter` |
|
||
| **Erinnern** | `memoro`, `journal`, `photos`, `moodlit`, `quotes` |
|
||
|
||
Alle Modul-IDs gegen `apps/mana/apps/web/src/lib/app-registry/apps.ts`
|
||
verifiziert (2026-04-23). Dedup über Templates hinweg passiert im
|
||
Finish-Handler; Prioritäts-Reihenfolge bleibt erhalten.
|
||
|
||
- „Fertig"-Button:
|
||
1. Union der `moduleIds` deduplicaten, Reihenfolge = erstes
|
||
Vorkommen in Template-Priorität
|
||
2. Cap bei **8 Modulen** (2×4 Grid) — Rest bleibt über „App
|
||
hinzufügen" erreichbar
|
||
3. `workbenchScenesStore.createScene({ name: 'Zuhause', apps })` —
|
||
überschreibt `DEFAULT_HOME_APPS` für diesen User
|
||
4. PATCH `onboardingCompletedAt = now()`
|
||
5. Redirect `/`
|
||
- „Überspringen": Scene wird **nicht** erstellt; fällt zurück auf
|
||
`DEFAULT_HOME_APPS`. `onboardingCompletedAt` wird **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` → setzt `onboardingCompletedAt = now()`,
|
||
idempotent (kein Update wenn bereits gesetzt)
|
||
- `PATCH /api/v1/me/onboarding/reset` → setzt auf `null`, für das
|
||
Settings-Re-Trigger in M5
|
||
|
||
**Shared-branding**: neue Datei
|
||
`packages/shared-branding/src/onboarding-templates.ts`:
|
||
|
||
```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 `/themes` extrahieren in
|
||
`ThemeSelector.svelte`, in beiden Screens mounten.
|
||
- **Module-Tiles** nutzen `APP_ICONS` aus
|
||
`packages/shared-branding/src/app-icons.ts` + `mana-apps.ts` fü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: `onboardingCompletedAt` column auf `auth.users`
|
||
- Endpoints: GET/POST/PATCH unter `/api/v1/me/onboarding/*`
|
||
- Client-Store: `$lib/stores/onboarding-status.svelte.ts` mit
|
||
`load()`/`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')` wenn
|
||
`onboardingStatus.completedAt === null` und Pfad nicht schon
|
||
`/onboarding/*`
|
||
- `/onboarding/+layout.svelte` mit 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.svelte` aus `/themes` extrahieren
|
||
- `/onboarding/look/+page.svelte`, Persistenz via
|
||
`userSettings.updateGlobal({theme})`
|
||
- E2E: Theme-Klick → CSS-Variablen wechseln live → reload → Theme bleibt
|
||
|
||
**M4 — Templates + Screen 3** (~1 Tag)
|
||
- `ONBOARDING_TEMPLATES` definieren, Modul-IDs gegen Registry
|
||
verifizieren (siehe Open Question 1)
|
||
- `/onboarding/templates/+page.svelte` mit 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-branding` gewinnt — 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 normale `AuthGate` beim Ö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
|
||
|
||
1. **Welcome-Page behalten oder löschen?** Als Marketing-Landing für
|
||
Logged-out-Pre-Signup könnte sie bleiben. Entscheidung in M5.
|
||
2. **Mobile (memoro)?** Onboarding ist v1 web-only; memoro könnte
|
||
`onboardingCompletedAt` + Templates künftig mit-konsumieren. Nicht
|
||
blockierend.
|
||
3. **Default-Mode Hell/Dunkel/System?** Aktueller Default ist unklar —
|
||
muss ich mit dem aktuellen Verhalten von `createThemeStore`
|
||
abgleichen, bevor Screen 2 fest verdrahtet ist. Wird in M3 geklärt
|
||
(ein-Zeiler-Lookup).
|