diff --git a/apps/mana/apps/web/src/routes/(app)/onboarding/templates/+page.svelte b/apps/mana/apps/web/src/routes/(app)/onboarding/templates/+page.svelte
new file mode 100644
index 000000000..eddd18e75
--- /dev/null
+++ b/apps/mana/apps/web/src/routes/(app)/onboarding/templates/+page.svelte
@@ -0,0 +1,351 @@
+
+
+
+
+
+
Wofür willst du Mana nutzen?
+
+ Wähl aus, was zu dir passt. Wir stellen dir dazu passende Module auf deinen Startbildschirm —
+ weitere kannst du jederzeit hinzufügen.
+
+
+
+
+ {#each ONBOARDING_TEMPLATES as tpl (tpl.id)}
+ {@const Icon = ICONS[tpl.iconName]}
+ {@const isSelected = selected.has(tpl.id)}
+
+ {/each}
+
+
+ {#if error}
+
{error}
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/shared-branding/src/index.ts b/packages/shared-branding/src/index.ts
index c5a70f917..4f12a27b1 100644
--- a/packages/shared-branding/src/index.ts
+++ b/packages/shared-branding/src/index.ts
@@ -84,3 +84,11 @@ export {
type SpaceModuleId,
type SpaceMetadata,
} from './spaces';
+
+// Onboarding templates (see docs/plans/onboarding-flow.md)
+export {
+ ONBOARDING_TEMPLATES,
+ resolveModulesForTemplates,
+ type OnboardingTemplate,
+ type OnboardingTemplateId,
+} from './onboarding-templates';
diff --git a/packages/shared-branding/src/onboarding-templates.spec.ts b/packages/shared-branding/src/onboarding-templates.spec.ts
new file mode 100644
index 000000000..7db8bc6f6
--- /dev/null
+++ b/packages/shared-branding/src/onboarding-templates.spec.ts
@@ -0,0 +1,100 @@
+import { describe, it, expect } from 'vitest';
+import {
+ ONBOARDING_TEMPLATES,
+ resolveModulesForTemplates,
+ type OnboardingTemplateId,
+} from './onboarding-templates';
+
+describe('ONBOARDING_TEMPLATES', () => {
+ it('has exactly 7 templates in the expected order', () => {
+ expect(ONBOARDING_TEMPLATES.map((t) => t.id)).toEqual([
+ 'alltag',
+ 'arbeit',
+ 'health',
+ 'sport',
+ 'lernen',
+ 'entdecken',
+ 'erinnern',
+ ]);
+ });
+
+ it('every template has a name, description, icon, and at least 3 modules', () => {
+ for (const tpl of ONBOARDING_TEMPLATES) {
+ expect(tpl.name.length).toBeGreaterThan(0);
+ expect(tpl.shortDescription.length).toBeGreaterThan(0);
+ expect(tpl.iconName.length).toBeGreaterThan(0);
+ expect(tpl.moduleIds.length).toBeGreaterThanOrEqual(3);
+ }
+ });
+
+ it('no template references duplicate module ids within itself', () => {
+ for (const tpl of ONBOARDING_TEMPLATES) {
+ expect(new Set(tpl.moduleIds).size).toBe(tpl.moduleIds.length);
+ }
+ });
+});
+
+describe('resolveModulesForTemplates', () => {
+ it('returns empty for empty selection', () => {
+ expect(resolveModulesForTemplates([])).toEqual([]);
+ });
+
+ it('returns a single templates modules verbatim (under cap)', () => {
+ expect(resolveModulesForTemplates(['alltag'])).toEqual([
+ 'todo',
+ 'calendar',
+ 'notes',
+ 'contacts',
+ ]);
+ });
+
+ it('dedupes across templates in priority order', () => {
+ // Alltag first: todo/calendar/notes/contacts
+ // Arbeit second: todo/calendar/mail/chat/times/notes — only mail/chat/times are new
+ const result = resolveModulesForTemplates(['alltag', 'arbeit']);
+ expect(result).toEqual(['todo', 'calendar', 'notes', 'contacts', 'mail', 'chat', 'times']);
+ });
+
+ it('preserves selection order (arbeit-first vs alltag-first)', () => {
+ const a = resolveModulesForTemplates(['arbeit', 'alltag']);
+ const b = resolveModulesForTemplates(['alltag', 'arbeit']);
+ // Different first module confirms priority is the selection order,
+ // not a fixed template order.
+ expect(a[0]).toBe('todo'); // both start with todo, but arbeit's "mail" outranks alltag's "contacts"
+ expect(a.indexOf('mail')).toBeLessThan(a.indexOf('contacts'));
+ expect(b.indexOf('contacts')).toBeLessThan(b.indexOf('mail'));
+ });
+
+ it('honours the cap and drops overflow modules (default cap = 8)', () => {
+ // All 7 templates picked — union pre-dedup is ~27 modules.
+ const allIds: OnboardingTemplateId[] = [
+ 'alltag',
+ 'arbeit',
+ 'health',
+ 'sport',
+ 'lernen',
+ 'entdecken',
+ 'erinnern',
+ ];
+ const result = resolveModulesForTemplates(allIds);
+ expect(result.length).toBe(8);
+ // Alltag's four modules must all make it — they're in the first template.
+ expect(result.slice(0, 4)).toEqual(['todo', 'calendar', 'notes', 'contacts']);
+ });
+
+ it('respects a custom cap', () => {
+ const result = resolveModulesForTemplates(['alltag', 'health'], 5);
+ expect(result.length).toBe(5);
+ expect(result).toEqual(['todo', 'calendar', 'notes', 'contacts', 'habits']);
+ });
+
+ it('silently ignores unknown template ids', () => {
+ const result = resolveModulesForTemplates([
+ 'alltag',
+ 'nonexistent' as OnboardingTemplateId,
+ 'arbeit',
+ ]);
+ // Same as 'alltag' + 'arbeit' without the unknown one.
+ expect(result).toEqual(['todo', 'calendar', 'notes', 'contacts', 'mail', 'chat', 'times']);
+ });
+});
diff --git a/packages/shared-branding/src/onboarding-templates.ts b/packages/shared-branding/src/onboarding-templates.ts
new file mode 100644
index 000000000..3617d1a49
--- /dev/null
+++ b/packages/shared-branding/src/onboarding-templates.ts
@@ -0,0 +1,120 @@
+/**
+ * Onboarding templates — starter-packs a user picks on Screen 3 of the
+ * first-login flow (/onboarding/templates). Each template is a named
+ * use-case with an ordered list of module IDs. The flow's finish
+ * handler deduplicates the union of picked templates' modules (keeping
+ * the first occurrence in priority order), caps the total at 8, and
+ * writes the result as the user's Home scene.
+ *
+ * All module IDs are verified against
+ * apps/mana/apps/web/src/lib/app-registry/apps.ts — if a template
+ * references a module that was removed, catch it in the test below
+ * before the user sees an empty tile.
+ *
+ * See docs/plans/onboarding-flow.md for rationale (why these templates,
+ * why multi-select, why a cap).
+ */
+
+export type OnboardingTemplateId =
+ | 'alltag'
+ | 'arbeit'
+ | 'health'
+ | 'sport'
+ | 'lernen'
+ | 'entdecken'
+ | 'erinnern';
+
+export type OnboardingTemplate = {
+ id: OnboardingTemplateId;
+ /** German name rendered on the tile. */
+ name: string;
+ /** One-line description under the tile name. */
+ shortDescription: string;
+ /** Phosphor icon name — resolved by the consuming page. */
+ iconName: 'House' | 'Briefcase' | 'Heart' | 'Barbell' | 'GraduationCap' | 'Compass' | 'Camera';
+ /**
+ * Module IDs in priority order (first = most important). Dedup
+ * across templates keeps the earliest occurrence; cap = 8.
+ */
+ moduleIds: string[];
+};
+
+export const ONBOARDING_TEMPLATES: readonly OnboardingTemplate[] = [
+ {
+ id: 'alltag',
+ name: 'Alltag',
+ shortDescription: 'Aufgaben, Termine, Notizen, Kontakte',
+ iconName: 'House',
+ moduleIds: ['todo', 'calendar', 'notes', 'contacts'],
+ },
+ {
+ id: 'arbeit',
+ name: 'Arbeit',
+ shortDescription: 'Produktivität für den Job',
+ iconName: 'Briefcase',
+ moduleIds: ['todo', 'calendar', 'mail', 'chat', 'times', 'notes'],
+ },
+ {
+ id: 'health',
+ name: 'Health',
+ shortDescription: 'Gesundheit, Stimmung, Ernährung, Zyklus',
+ iconName: 'Heart',
+ moduleIds: ['habits', 'body', 'mood', 'food', 'period'],
+ },
+ {
+ id: 'sport',
+ name: 'Sport',
+ shortDescription: 'Training, Ziele, Körper, Ernährung',
+ iconName: 'Barbell',
+ moduleIds: ['habits', 'body', 'food', 'goals', 'stretch'],
+ },
+ {
+ id: 'lernen',
+ name: 'Lernen',
+ shortDescription: 'Skills, Quizzes, Notizen, Kontext',
+ iconName: 'GraduationCap',
+ moduleIds: ['skilltree', 'quiz', 'notes', 'library', 'kontext'],
+ },
+ {
+ id: 'entdecken',
+ name: 'Entdecken',
+ shortDescription: 'Orte, Fotos, Musik, Wetter',
+ iconName: 'Compass',
+ moduleIds: ['places', 'citycorners', 'photos', 'music', 'wetter'],
+ },
+ {
+ id: 'erinnern',
+ name: 'Erinnern',
+ shortDescription: 'Memoro, Journal, Fotos, Zitate',
+ iconName: 'Camera',
+ moduleIds: ['memoro', 'journal', 'photos', 'moodlit', 'quotes'],
+ },
+];
+
+/**
+ * Pick modules for the user's Home scene based on a multi-selection of
+ * templates. Iterates templates in the order provided, collecting their
+ * modules while skipping dups and stopping at the cap.
+ *
+ * Exported so the onboarding page and its test can share the same
+ * logic — the page only has to worry about UI state.
+ */
+export function resolveModulesForTemplates(
+ selectedIds: readonly OnboardingTemplateId[],
+ cap = 8
+): string[] {
+ const byId = new Map(ONBOARDING_TEMPLATES.map((t) => [t.id, t]));
+ const seen = new Set();
+ const out: string[] = [];
+ for (const id of selectedIds) {
+ const tpl = byId.get(id);
+ if (!tpl) continue;
+ for (const moduleId of tpl.moduleIds) {
+ if (seen.has(moduleId)) continue;
+ seen.add(moduleId);
+ out.push(moduleId);
+ if (out.length >= cap) return out;
+ }
+ }
+ return out;
+}