diff --git a/docs/plans/feedback-hub.md b/docs/plans/feedback-hub.md
new file mode 100644
index 000000000..76e927eed
--- /dev/null
+++ b/docs/plans/feedback-hub.md
@@ -0,0 +1,236 @@
+---
+status: draft
+owner: till
+created: 2026-04-26
+---
+
+# `@mana/feedback` als zentraler Feedback-Hub
+
+> Alle nutzergenerierten Rückmeldungen — Bug-Reports, Feature-Wünsche, Lob,
+> Onboarding-Wünsche, NPS, Churn-Gründe — landen in einem System: dem
+> `user_feedback`-Table im `mana-analytics`-Service, getypt über das
+> `@mana/feedback`-Package. Ein Schema, ein API-Surface, ein Admin-Hub.
+
+---
+
+## Ist-Zustand (2026-04-26)
+
+- **Package** `packages/feedback/` exportiert Typen, einen Service-Factory
+ (`createFeedbackService`), und UI-Komponenten (`FeedbackPage`,
+ `FeedbackForm`, `FeedbackList`, `VoteButton`, `StatusBadge`).
+- **Server** `services/mana-analytics` (Port 3064): Postgres-Schema
+ `feedback`, Tabellen `user_feedback` + `feedback_votes`, REST-Endpoints
+ unter `/api/v1/feedback/*`. Auto-Title via `mana-llm` beim Submit.
+- **Web-App** Singleton in `apps/mana/apps/web/src/lib/api/feedback.ts`,
+ Modul-View in `apps/mana/apps/web/src/lib/modules/feedback/ListView.svelte`,
+ Route `/feedback`. Voting + Public-Liste sind voll verdrahtet.
+- **Drift**, die fixen müssen, bevor wir mehr draufpacken:
+ | Bereich | Package | DB |
+ |----------|--------------------------------------------|--------------------------------------|
+ | Status | `submitted/under_review/.../completed/declined` | `new/reviewed/.../done/rejected` |
+ | Category | `bug/feature/improvement/question/other` | + `praise` (zusätzlich) |
+ | Default | `submitted` (impliziert) | `new` |
+ Konsequenz: Client typisiert Status als `'submitted'`, kriegt aber
+ `'new'` zurück → `FEEDBACK_STATUS_CONFIG[status]` ist `undefined` →
+ StatusBadge rendert leise nichts. Niemand fällt's auf weil keine
+ Admin-UI Status setzt und alle Records auf Default sitzen.
+- **Fundamentale Annahmen, die wir nicht ändern:**
+ - Server-only Persistence (kein Dexie / Local-First, kein mana-sync).
+ Submit ist ein einziger POST, fail-soft.
+ - Feedback ist **nicht** im Mana-Crypto-Pfad — Klartext im DB.
+ OK für Bug-Reports & Wünsche; sensible Daten gehören eh nicht hier rein.
+
+---
+
+## Phase 0 — Drift fixen *(Refactor, ein Commit)*
+
+Ziel: Package + DB konsistent, Defaults sauber, keine Funktions-Erweiterung.
+
+### 0a. Status-Enum: Package gewinnt
+
+PostgreSQL kann seit 10 `ALTER TYPE ... RENAME VALUE`, das ist non-destructive
+und behält die Sortierung der Enum-Werte. Wir benennen die DB-Werte um, sodass
+sie zum Package passen:
+
+```sql
+ALTER TYPE feedback.feedback_status RENAME VALUE 'new' TO 'submitted';
+ALTER TYPE feedback.feedback_status RENAME VALUE 'reviewed' TO 'under_review';
+ALTER TYPE feedback.feedback_status RENAME VALUE 'done' TO 'completed';
+ALTER TYPE feedback.feedback_status RENAME VALUE 'rejected' TO 'declined';
+ALTER TABLE feedback.user_feedback ALTER COLUMN status SET DEFAULT 'submitted';
+```
+
+Drizzle-Schema (`services/mana-analytics/src/db/schema/feedback.ts`) parallel
+auf die neuen Werte ziehen, sodass `db:push` nicht versucht, neu anzulegen.
+
+### 0b. Category 'praise' ins Package aufnehmen
+
+Package hat `bug/feature/improvement/question/other`. DB hat `praise`
+zusätzlich. Wir nehmen `'praise'` ins Package mit Label "Lob" und
+behalten DB unverändert.
+
+### 0c. Single Source of Truth
+
+Mana-analytics importiert die Enum-Werte ab jetzt aus `@mana/feedback`
+statt eigenes `pgEnum`-Array zu pflegen. Verhindert künftige Drift
+strukturell. (Falls drizzle-kit das nicht direkt kann, dann mindestens
+ein Test in `services/mana-analytics` der die Listen vergleicht.)
+
+### Migrations-Workflow
+
+mana-analytics benutzt aktuell `drizzle-kit push` (kein
+Migrations-Verzeichnis). Für `ALTER TYPE RENAME VALUE` ist push nicht
+zuverlässig — das ist ein hand-authored SQL-Step.
+
+→ Neue Datei `services/mana-analytics/drizzle/0001_align-feedback-enums.sql`
+einführen, in Setup-README dokumentieren ("apply manually before db:push").
+Pattern wie `apps/api/drizzle/{schema}/*.sql`.
+
+---
+
+## Phase 1 — Onboarding-Wish *(Feature, ein Commit)*
+
+Ziel: Letzter Onboarding-Schritt ist Freitext-Frage "Was wünschst du dir
+von Mana?", deren Antwort als `@mana/feedback`-Record landet.
+
+### 1a. Neue Category `'onboarding-wish'`
+
+```sql
+ALTER TYPE feedback.feedback_category ADD VALUE IF NOT EXISTS 'onboarding-wish';
+```
+
+Im Package:
+- `FeedbackCategory` um `'onboarding-wish'` erweitern
+- `FEEDBACK_CATEGORY_LABELS['onboarding-wish'] = 'Was ich mir wünsche'`
+
+### 1b. Onboarding-Flow-Store erweitern
+
+`apps/mana/apps/web/src/lib/stores/onboarding-flow.svelte.ts`:
+- `pendingWish: string | null`
+- `setPendingWish(value)` / `reset()` mit dabei
+
+### 1c. Layout: 3 → 4 Step-Dots
+
+`apps/mana/apps/web/src/routes/(app)/onboarding/+layout.svelte`:
+- `currentStep`-Mapping: `/onboarding/wish` → 3
+- Dots-Array `[0,1,2,3]`
+- aria-valuemax = 4
+
+### 1d. Neuer Screen `/onboarding/wish/+page.svelte`
+
+- **Aktivierungstext:**
+ > # Eine letzte Sache
+ > Was wünschst du dir von Mana? Wofür willst du's nutzen, was erhoffst du dir?
+ >
+ > Schreib einfach, wie's dir kommt — wir lesen jede Antwort und sie
+ > hilft uns, Mana für dich besser zu machen.
+- **Textarea**: `maxlength=2000`, autofocus, `auto-grow`
+- **Buttons**: Zurück (→ `/onboarding/templates`) + Fertig
+- **Submit-Logik** (Fertig):
+ 1. Wenn Textarea nicht leer → `feedbackService.createFeedback({
+ category: 'onboarding-wish', isPublic: false, feedbackText: trimmed })`
+ **fail-soft** (`try/catch`, nur `console.warn`, kein UI-Block)
+ 2. `onboardingStatus.markComplete()`
+ 3. `onboardingFlow.reset()`
+ 4. `goto('/')`
+- **Wenn Textarea leer + Fertig**: gleicher Flow ohne Submit.
+- **isPublic = false** by default, weil Wünsche persönliche Statements sind,
+ kein Public-Voting-Material. (Lässt sich später per Admin-Action publishen.)
+
+### 1e. Templates-Screen umbiegen
+
+`apps/mana/apps/web/src/routes/(app)/onboarding/templates/+page.svelte`:
+- Fertig-Button heißt jetzt "Weiter" und routet `goto('/onboarding/wish')`
+- `markComplete` + `reset` wandern raus aus templates → in den wish-Screen
+- Templates-Save (createScene) bleibt wie er ist
+
+### Akzeptanzkriterien
+
+- 4 Step-Dots im Footer; bei `/onboarding/wish` ist Dot 4 aktiv
+- Globaler Skip-Button (unten links) funktioniert auf allen 4 Screens
+ (markComplete + `/`)
+- Submit von "Was wünschst du dir" landet als Row in `user_feedback`
+ mit `category='onboarding-wish'`, `is_public=false`
+- Wenn `mana-analytics` nicht erreichbar ist, blockiert das Onboarding
+ nicht — User kommt trotzdem auf `/`
+- Bestehende Public-Feedback-Liste auf `/feedback` zeigt
+ `onboarding-wish`-Records **nicht** (weil `is_public=false`)
+
+---
+
+## Phase 2 — Hub-Charakter ausbauen *(separater Sprint)*
+
+Drei kleine Erweiterungen, die `@mana/feedback` zur "echten" Zentrale
+machen. Können einzeln geshippt werden.
+
+### 2a. Globaler Feedback-Button
+
+Eintrag im Account-Menü oder PillNav ("Feedback / Idee teilen") öffnet
+einen Modal mit der bestehenden `FeedbackForm`. Eliminiert das Risiko,
+dass jemand pro Modul eigene Feedback-Buttons baut.
+
+### 2b. Inline-Hook pro Modul
+
+Komponente `` für Module-Help-Panels,
+vorausgefüllt mit `appId`, Default-Category `'feature' | 'improvement'`.
+Kontextspezifische Wünsche.
+
+### 2c. Admin-Triage-Hub `/feedback/admin`
+
+Founder-Tier-gated. Features:
+- Filter: Kategorie, Status, Datum, App
+- Bulk-Status-Updates
+- `adminResponse` schreiben
+- Aggregations-Card für `onboarding-wish`: alle Antworten gelistet,
+ optional via LLM nach Themen geclustert (was wollen neue Nutzer
+ am häufigsten?)
+- Neue Backend-Endpoints:
+ - `PATCH /api/v1/feedback/admin/:id` (status, adminResponse, isPublic)
+ - `GET /api/v1/feedback/admin?category=...&status=...` (alle inkl. private)
+
+---
+
+## Phase 3 — Future Categories *(Backlog, Schema-Slot offenhalten)*
+
+Damit das Schema nicht noch mal bricht, halten wir Platz für:
+
+- `'nps'` — Score 0-10 + optional Kommentar; nach 30 Tagen aktiver Nutzung
+ einmalig getriggert. Brauche dafür eine optionale `score INT`-Spalte
+ auf `user_feedback`.
+- `'churn-feedback'` — wenn jemand den Account löscht: warum?
+ (Pflicht-Modal vor Final-Delete.)
+- `'support-request'` — 1:1-Hilfe statt öffentlicher Bug.
+- `'praise'` — schon in Phase 0 mitgenommen.
+
+→ NICHT jetzt bauen, nur als Roadmap-Marker.
+
+---
+
+## Phase 4 — Local-First *(deferred)*
+
+Aktuell ist `@mana/feedback` Server-Direct-POST. Local-First lohnt sich erst,
+wenn:
+- Leute Feedback offline schreiben sollen (Mobile-Use-Case)
+- `feedbackText` verschlüsselt im Sync laufen soll (privacy-relevant?)
+
+Bis dahin: Status quo. Dexie-Tabelle `feedbackEntries` + `crypto/registry.ts`
++ mana-sync field-level LWW wäre der Migrations-Plan.
+
+---
+
+## Bekannte Drift, die wir hier NICHT angehen
+
+- **`apps/mana/apps/web/src/lib/api/feedback.ts`** schickt an
+ `getManaAuthUrl()/api/v1/feedback`. Aber `mana-analytics` (3064) ist die
+ echte Heimat. Funktioniert nur, wenn mana-auth proxiet. Eigener Fix-PR.
+- **Feedback fehlt in `packages/shared-branding/src/mana-apps.ts`** —
+ ist nur in `apps/web/src/lib/app-registry/apps.ts`. Konsistent oder
+ bewusst? Nicht in dieser Plan-Iteration.
+
+---
+
+## Reihenfolge & Commits
+
+1. **Commit 1 (Phase 0)**: `refactor(feedback): align package + DB enums, add 'praise' category`
+2. **Commit 2 (Phase 1)**: `feat(onboarding): add wish step, route to feedback service`
+3. **Phase 2/3/4**: separate Sprints, separate Plan-Updates.
diff --git a/packages/feedback/src/api.ts b/packages/feedback/src/api.ts
index 53e961dd1..7467d9026 100644
--- a/packages/feedback/src/api.ts
+++ b/packages/feedback/src/api.ts
@@ -8,6 +8,12 @@ export interface CreateFeedbackInput {
title?: string;
feedbackText: string;
category?: FeedbackCategory;
+ /**
+ * Whether the submission shows up in the public community list.
+ * Defaults to `true` server-side. Set `false` for private intake
+ * categories like `onboarding-wish` or `churn-feedback`.
+ */
+ isPublic?: boolean;
deviceInfo?: Record;
}
diff --git a/packages/feedback/src/feedback.ts b/packages/feedback/src/feedback.ts
index 731678592..29a9f861f 100644
--- a/packages/feedback/src/feedback.ts
+++ b/packages/feedback/src/feedback.ts
@@ -1,8 +1,20 @@
/**
- * Core feedback types
+ * Core feedback types — Single source of truth for the @mana/feedback hub.
+ *
+ * Mana-analytics' Postgres enums (`feedback.feedback_category`,
+ * `feedback.feedback_status`) MUST mirror these literal unions exactly.
+ * If you add or rename a value here, also write a SQL migration under
+ * services/mana-analytics/drizzle/.
*/
-export type FeedbackCategory = 'bug' | 'feature' | 'improvement' | 'question' | 'other';
+export type FeedbackCategory =
+ | 'bug'
+ | 'feature'
+ | 'improvement'
+ | 'question'
+ | 'praise'
+ | 'onboarding-wish'
+ | 'other';
export type FeedbackStatus =
| 'submitted'
@@ -43,6 +55,8 @@ export const FEEDBACK_CATEGORY_LABELS: Record = {
feature: 'Feature',
improvement: 'Verbesserung',
question: 'Frage',
+ praise: 'Lob',
+ 'onboarding-wish': 'Was ich mir wünsche',
other: 'Sonstiges',
};
diff --git a/services/mana-analytics/drizzle/0001_align-feedback-enums.sql b/services/mana-analytics/drizzle/0001_align-feedback-enums.sql
new file mode 100644
index 000000000..787e3cdf0
--- /dev/null
+++ b/services/mana-analytics/drizzle/0001_align-feedback-enums.sql
@@ -0,0 +1,66 @@
+-- 0001_align-feedback-enums.sql
+--
+-- Bringt die Postgres-Enums `feedback.feedback_category` und
+-- `feedback.feedback_status` mit dem `@mana/feedback`-Package in Einklang
+-- und legt die `onboarding-wish`-Kategorie für den letzten
+-- Onboarding-Schritt an.
+--
+-- Hand-authored, weil `drizzle-kit push` Enum-Werte nicht zuverlässig
+-- umbenennt. Apply manually before next `pnpm db:push`:
+--
+-- psql "$DATABASE_URL" -f services/mana-analytics/drizzle/0001_align-feedback-enums.sql
+--
+-- Idempotent: alle Schritte verwenden `IF EXISTS` / `IF NOT EXISTS`-Checks
+-- via DO-Blöcke, sodass Re-Runs nicht failen.
+
+BEGIN;
+
+-- 1. Status-Werte umbenennen, sodass sie zum Package passen.
+-- PostgreSQL ≥10 supportet ALTER TYPE … RENAME VALUE non-destructive.
+DO $$
+BEGIN
+ IF EXISTS (
+ SELECT 1 FROM pg_enum e
+ JOIN pg_type t ON t.oid = e.enumtypid
+ JOIN pg_namespace n ON n.oid = t.typnamespace
+ WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'new'
+ ) THEN
+ ALTER TYPE feedback.feedback_status RENAME VALUE 'new' TO 'submitted';
+ END IF;
+
+ IF EXISTS (
+ SELECT 1 FROM pg_enum e
+ JOIN pg_type t ON t.oid = e.enumtypid
+ JOIN pg_namespace n ON n.oid = t.typnamespace
+ WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'reviewed'
+ ) THEN
+ ALTER TYPE feedback.feedback_status RENAME VALUE 'reviewed' TO 'under_review';
+ END IF;
+
+ IF EXISTS (
+ SELECT 1 FROM pg_enum e
+ JOIN pg_type t ON t.oid = e.enumtypid
+ JOIN pg_namespace n ON n.oid = t.typnamespace
+ WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'done'
+ ) THEN
+ ALTER TYPE feedback.feedback_status RENAME VALUE 'done' TO 'completed';
+ END IF;
+
+ IF EXISTS (
+ SELECT 1 FROM pg_enum e
+ JOIN pg_type t ON t.oid = e.enumtypid
+ JOIN pg_namespace n ON n.oid = t.typnamespace
+ WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'rejected'
+ ) THEN
+ ALTER TYPE feedback.feedback_status RENAME VALUE 'rejected' TO 'declined';
+ END IF;
+END
+$$;
+
+-- 2. Default für status auf den neuen Wert setzen.
+ALTER TABLE feedback.user_feedback ALTER COLUMN status SET DEFAULT 'submitted';
+
+-- 3. Neue Category für Onboarding-Wishes anlegen.
+ALTER TYPE feedback.feedback_category ADD VALUE IF NOT EXISTS 'onboarding-wish';
+
+COMMIT;
diff --git a/services/mana-analytics/src/db/schema/feedback.ts b/services/mana-analytics/src/db/schema/feedback.ts
index 770d4895d..d1100b443 100644
--- a/services/mana-analytics/src/db/schema/feedback.ts
+++ b/services/mana-analytics/src/db/schema/feedback.ts
@@ -13,22 +13,27 @@ import {
export const feedbackSchema = pgSchema('feedback');
+// Enum values must mirror @mana/feedback's FeedbackCategory / FeedbackStatus
+// unions exactly. Renames or additions need a hand-authored SQL migration
+// under services/mana-analytics/drizzle/ (drizzle-kit push can't safely
+// rename enum values).
export const feedbackCategoryEnum = pgEnum('feedback_category', [
'bug',
'feature',
'improvement',
'question',
'praise',
+ 'onboarding-wish',
'other',
]);
export const feedbackStatusEnum = pgEnum('feedback_status', [
- 'new',
- 'reviewed',
+ 'submitted',
+ 'under_review',
'planned',
'in_progress',
- 'done',
- 'rejected',
+ 'completed',
+ 'declined',
]);
export const userFeedback = feedbackSchema.table(
@@ -40,7 +45,7 @@ export const userFeedback = feedbackSchema.table(
title: text('title'),
feedbackText: text('feedback_text').notNull(),
category: feedbackCategoryEnum('category').default('other').notNull(),
- status: feedbackStatusEnum('status').default('new').notNull(),
+ status: feedbackStatusEnum('status').default('submitted').notNull(),
isPublic: boolean('is_public').default(true).notNull(),
adminResponse: text('admin_response'),
voteCount: integer('vote_count').default(0).notNull(),
diff --git a/services/mana-analytics/src/services/feedback.ts b/services/mana-analytics/src/services/feedback.ts
index 1ec1acaec..6f18a528f 100644
--- a/services/mana-analytics/src/services/feedback.ts
+++ b/services/mana-analytics/src/services/feedback.ts
@@ -20,6 +20,7 @@ export class FeedbackService {
feedbackText: string;
category?: string;
title?: string;
+ isPublic?: boolean;
deviceInfo?: Record;
}
) {
@@ -42,6 +43,10 @@ export class FeedbackService {
title: title || data.feedbackText.slice(0, 80),
feedbackText: data.feedbackText,
category: (data.category as any) || 'other',
+ // Honor explicit isPublic from caller; otherwise let the column
+ // default (true) apply. Private intake categories like
+ // 'onboarding-wish' should pass `false`.
+ ...(typeof data.isPublic === 'boolean' ? { isPublic: data.isPublic } : {}),
deviceInfo: data.deviceInfo,
})
.returning();