diff --git a/CLAUDE.md b/CLAUDE.md
index 06fbe4b..db7f837 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -96,19 +96,468 @@ mitai-jinkendo/
- ✅ Automatische SQLite→PostgreSQL Migration bei Container-Start
- ✅ **Modulare Backend-Architektur**: 14 Router-Module, main.py von 1878→75 Zeilen (-96%)
-### Was in v9c kommt:
-- 🔲 Selbst-Registrierung mit E-Mail-Bestätigung
-- 🔲 Freemium Tier-System (free/basic/premium/selfhosted)
-- 🔲 14-Tage Trial automatisch
-- 🔲 Einladungslinks für Beta-Nutzer
-- 🔲 Admin kann Tiers manuell setzen
+### Was in v9c kommt: Subscription & Coupon Management System
+**Core Features:**
+- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung (Pflicht)
+- 🔲 Flexibles Tier-System (free/basic/premium/selfhosted) - Admin-editierbar
+- 🔲 Trial-System (Dauer konfigurierbar, auto-start nach E-Mail-Verifikation)
+- 🔲 **Coupon-System** (2 Typen):
+ - Single-Use Coupons (Geschenke, zeitlich begrenzt)
+ - Multi-Use Period Coupons (z.B. Wellpass, monatlich erneuerbar)
+- 🔲 Coupon-Stacking-Logik (Pause + Resume bei Wellpass-Override)
+- 🔲 Access-Grant-System (zeitlich begrenzte Zugriffe mit Quelle-Tracking)
+- 🔲 User-Activity-Log (Login, Password-Änderungen, Coupon-Einlösungen, etc.)
+- 🔲 User-Stats (Login-Streaks, Nutzungsstatistiken)
+- 🔲 Individuelle User-Restrictions (Admin kann Limits pro User setzen)
+- 🔲 App-Settings (globale Konfiguration durch Admin)
+- 🔲 Erweiterte Admin-User-Verwaltung (Activity-Log, Stats, Access-Historie)
+
+**E-Mail Templates (v9c):**
+- 🔲 Registrierung + E-Mail-Verifizierung
+- 🔲 Einladungslink
+- 🔲 Passwort-Reset (bereits vorhanden)
+
+**Spätere Features (v9d/v9e):**
+- 🔲 Bonus-System (Login-Streaks → Punkte → Geschenk-Coupons)
+- 🔲 Trial-Reminder-E-Mails (3 Tage vor Ablauf)
+- 🔲 Monatliches Nutzungs-Summary per E-Mail
+- 🔲 Self-Service Upgrade (Stripe-Integration)
+- 🔲 Partner-Verwaltung (Wellpass, Hansefit, etc.)
+- 🔲 Admin-Benachrichtigungen (neue Registrierungen, etc.)
### Was in v9d kommt:
+- 🔲 Bonus-System & Gamification (Streaks, Achievements)
+- 🔲 Stripe-Integration (Self-Service Upgrade, Subscriptions)
- 🔲 OAuth2-Grundgerüst für Fitness-Connectoren
- 🔲 Strava Connector
- 🔲 Withings Connector (Waage)
- 🔲 Garmin Connector
+---
+
+## v9c Architektur-Details: Subscription & Coupon System
+
+### Datenbank-Schema (Neue Tabellen)
+
+#### **app_settings** - Globale Konfiguration
+```sql
+key, value, value_type, description, updated_at, updated_by
+
+Beispiele:
+- trial_days: 14
+- trial_behavior: 'downgrade' | 'lock'
+- allow_registration: true/false
+- default_tier_trial: 'premium'
+- gift_coupons_per_month: 3
+```
+
+#### **tiers** - Tier-Konfiguration (vereinfacht)
+```sql
+id, slug, name, description, sort_order, active
+
+Initial Tiers:
+- free, basic, premium, selfhosted
+
+Limits sind jetzt in tier_limits Tabelle (siehe unten)!
+```
+
+#### **features** - Feature-Registry (alle limitierbaren Features)
+```sql
+id, slug, name, category, description, unit
+default_limit (NULL = unbegrenzt)
+reset_period ('monthly' | 'daily' | 'never')
+visible_in_admin, sort_order, active
+
+Initial Features:
+- weight_entries: Gewichtseinträge, default: 30, never
+- circumference_entries: Umfangsmessungen, default: 30, never
+- caliper_entries: Caliper-Messungen, default: 30, never
+- nutrition_entries: Ernährungseinträge, default: 30, never
+- activity_entries: Aktivitäten, default: 30, never
+- photos: Progress-Fotos, default: 5, never
+- ai_calls: KI-Analysen, default: 0, monthly
+- ai_pipeline: KI-Pipeline, default: 0, monthly
+- csv_import: CSV-Importe, default: 0, monthly
+- data_export: Daten-Exporte, default: 0, monthly
+- fitness_connectors: Fitness-Connectoren, default: 0, never
+
+Neue Features einfach per INSERT hinzufügen - kein Schema-Change!
+```
+
+#### **tier_limits** - Limits pro Tier + Feature
+```sql
+id, tier_slug, feature_slug, limit_value, enabled
+
+Beispiel Free Tier:
+- ('free', 'weight_entries', 30, true)
+- ('free', 'ai_calls', 0, false) -- KI deaktiviert
+- ('free', 'data_export', 0, false)
+
+Beispiel Premium:
+- ('premium', 'weight_entries', NULL, true) -- unbegrenzt
+- ('premium', 'ai_calls', NULL, true) -- unbegrenzt
+
+Admin kann in UI Matrix bearbeiten: Tier x Feature
+```
+
+#### **user_feature_restrictions** - Individuelle User-Limits
+```sql
+id, profile_id, feature_slug, limit_value, enabled
+reason, set_by (admin_id)
+
+Überschreibt Tier-Limits für spezifische User.
+Admin kann jeden User individuell einschränken oder erweitern.
+```
+
+#### **user_feature_usage** - Nutzungs-Tracking
+```sql
+id, profile_id, feature_slug, period_start, usage_count, last_used
+
+Für Features mit reset_period (z.B. ai_calls monthly).
+Wird automatisch zurückgesetzt am Monatsanfang.
+```
+
+#### **coupons** - Coupon-Verwaltung
+```sql
+code, type ('single_use' | 'multi_use_period' | 'gift')
+valid_from, valid_until, grants_tier, duration_days
+max_redemptions, current_redemptions
+created_by, created_for, notes, active
+
+Beispiel Single-Use:
+ Code: FRIEND-GIFT-XYZ, 30 Tage Premium, max 1x
+
+Beispiel Multi-Use Period:
+ Code: WELLPASS-2026-03, gültig 01.03-31.03, unbegrenzte Einlösungen
+```
+
+#### **coupon_redemptions** - Einlösungs-Historie
+```sql
+coupon_id, profile_id, redeemed_at, access_grant_id
+UNIQUE(coupon_id, profile_id) - User kann denselben Coupon nur 1x einlösen
+```
+
+#### **access_grants** - Zeitlich begrenzte Zugriffe
+```sql
+profile_id, granted_tier, valid_from, valid_until
+source ('coupon' | 'admin_grant' | 'trial')
+active (false wenn pausiert durch Wellpass-Override)
+paused_at, paused_by (access_grant_id das pausiert hat)
+
+Stacking-Logik:
+- Multi-Use Period Coupon (Wellpass): pausiert andere grants
+- Single-Use Coupon: stackt zeitlich (Resume nach Ablauf)
+```
+
+#### **user_activity_log** - Aktivitäts-Tracking
+```sql
+profile_id, activity_type, details (JSONB), ip_address, user_agent, created
+
+Activity Types:
+- login, password_change, email_change, coupon_redeemed
+- tier_change, export, ai_analysis, registration
+```
+
+#### **user_stats** - Aggregierte Statistiken
+```sql
+profile_id, first_login, last_login, total_logins
+current_streak_days, longest_streak_days, last_streak_date
+total_weight_entries, total_ai_analyses, total_exports
+bonus_points (später), gift_coupons_available (später)
+```
+
+#### **profiles** - Erweiterte Spalten
+```sql
+tier, tier_locked (Admin kann Tier festnageln)
+trial_ends_at, email_verified, email_verify_token
+invited_by, contract_type, contract_valid_until
+stripe_customer_id (vorbereitet für v9d)
+```
+
+---
+
+### Backend-Erweiterungen
+
+#### Neue Router (v9c):
+```
+routers/tiers.py - Tier-Verwaltung (List, Edit, Create)
+routers/features.py - Feature-Registry (List, Add, Edit, Delete) ⭐ NEU
+routers/tier_limits.py - Tier-Limits-Matrix (Admin bearbeitet Tier x Feature) ⭐ NEU
+routers/coupons.py - Coupon-System (Redeem, Admin CRUD)
+routers/access_grants.py - Zugriffs-Verwaltung (Current, Grant, Revoke)
+routers/user_admin.py - Erweiterte User-Verwaltung (Activity, Stats, Feature-Restrictions)
+routers/settings.py - App-Einstellungen (Admin)
+routers/registration.py - Registrierung + E-Mail-Verifizierung
+```
+
+#### Neue Middleware:
+```python
+check_feature_access(profile_id, feature_slug, action='use')
+ """
+ Zentrale Feature-Access-Prüfung.
+ Hierarchie:
+ 1. User-Restriction (höchste Priorität)
+ 2. Tier-Limit
+ 3. Feature-Default
+
+ Returns: {'allowed': bool, 'limit': int, 'used': int, 'remaining': int, 'reason': str}
+ """
+
+increment_feature_usage(profile_id, feature_slug)
+ """
+ Inkrementiert Nutzungszähler.
+ Berücksichtigt reset_period (monthly, daily, never).
+ """
+
+log_activity(profile_id, activity_type, details=None)
+ """
+ Loggt User-Aktivitäten in user_activity_log.
+ """
+```
+
+#### Hintergrund-Tasks (Cron):
+```python
+check_expired_access() # Täglich 00:00 - Trial/Coupon-Ablauf prüfen
+reset_monthly_limits() # 1. jeden Monats - AI-Calls zurücksetzen
+update_user_streaks() # Täglich 23:59 - Login-Streaks aktualisieren
+```
+
+---
+
+### Zugriffs-Hierarchie
+
+```
+Effektiver Tier wird ermittelt durch (Priorität absteigend):
+1. Admin-Override (tier_locked=true) → nutzt profiles.tier
+2. Aktiver access_grant (nicht pausiert, valid_until > now)
+3. Trial (trial_ends_at > now)
+4. Base tier (profiles.tier)
+
+Wellpass-Override-Logik:
+- User hat Single-Use Coupon (20 Tage verbleibend)
+- User löst Wellpass-Coupon ein (gültig bis 31.03)
+- Single-Use access_grant wird pausiert (active=false, paused_by=wellpass_grant_id)
+- Nach Wellpass-Ablauf: Single-Use wird reaktiviert (noch 20 Tage)
+```
+
+---
+
+### Tier-Limits & Feature-Gates
+
+**Daten-Sichtbarkeit bei Downgrade:**
+- Frontend: Buttons/Features ausblenden (Export, KI, Import)
+- Backend: API limitiert Rückgabe (z.B. nur letzte 30 Gewichtseinträge bei free)
+- Daten bleiben erhalten, werden nur versteckt
+- Bei Upgrade wieder sichtbar
+
+**Feature-Checks:**
+```python
+# Beispiel: Gewicht-Eintrag erstellen
+@check_feature_limit('weight', 'create')
+def create_weight_entry():
+ # Prüft: Hat User max_weight_entries erreicht?
+ # Falls ja: HTTPException 403 "Limit erreicht - Upgrade erforderlich"
+```
+
+---
+
+### Frontend-Erweiterungen
+
+#### Neue Seiten:
+```
+RegisterPage.jsx - Registrierung (Name, E-Mail, Passwort)
+VerifyEmailPage.jsx - E-Mail-Verifizierung (Token aus URL)
+RedeemCouponPage.jsx - Coupon-Eingabe (oder Modal)
+AdminCouponsPage.jsx - Coupon-Verwaltung (Admin)
+AdminTiersPage.jsx - Tier-Verwaltung (CRUD) (Admin)
+AdminFeaturesPage.jsx - Feature-Registry (List, Add, Edit) ⭐ NEU
+AdminTierLimitsPage.jsx - Tier x Feature Matrix (bearbeiten) ⭐ NEU
+AdminUserRestrictionsPage.jsx - User-spezifische Limits (bearbeiten) ⭐ NEU
+AdminSettingsPage.jsx - App-Einstellungen (Admin)
+```
+
+#### Neue Komponenten:
+```jsx
+ // Tier-Anzeige mit Icon
+...> // Feature-basierte Sichtbarkeit ⭐ GEÄNDERT
+ // "Trial endet in 5 Tagen" Banner
+ // Coupon-Eingabefeld
+ // User-Activity-Log
+ // "5/10 verwendet" Anzeige ⭐ NEU
+ // Matrix-Editor ⭐ NEU
+ // Login-Streak (später)
+```
+
+#### Erweiterte Admin-Seiten:
+```
+AdminUsersPage.jsx erweitert um:
+- Activity-Log Button → zeigt user_activity_log
+- Stats Button → zeigt user_stats
+- Access-Grants Button → zeigt aktive/abgelaufene Zugriffe
+- Feature-Restrictions Button → individuelle Feature-Limits setzen ⭐ GEÄNDERT
+- Grant Access Button → manuell Tier-Zugriff gewähren
+- Usage-Overview → zeigt user_feature_usage für alle Features ⭐ NEU
+```
+
+#### Admin-Interface-Details:
+
+**AdminFeaturesPage.jsx** - Feature-Registry verwalten
+```jsx
+// Alle Features auflisten + neue hinzufügen
+
+ {features.map(f => (
+
+ {f.name}
+ {f.category}
+ {f.unit}
+ {f.reset_period}
+ {f.default_limit ?? '∞'}
+
+
+
+
+
+ ))}
+
+
+```
+
+**AdminTierLimitsPage.jsx** - Matrix-Editor
+```jsx
+// Matrix-View: Tiers (Spalten) x Features (Zeilen)
+
+
+
+ | Feature |
+ Free |
+ Basic |
+ Premium |
+ Selfhosted |
+
+
+
+
+ | Gewichtseinträge |
+ |
+ |
+ |
+ |
+
+
+ | KI-Analysen/Monat |
+ 0 |
+ |
+ ∞ |
+ ∞ |
+
+
+
+```
+
+**AdminUserRestrictionsPage.jsx** - Individuelle User-Limits
+```jsx
+
+
+
+
+ {features.map(f => (
+
+ {f.name}
+ {getTierLimit(user.tier, f.slug)}
+ {getUsage(user.id, f.slug)}
+
+
+
+
+
+ ))}
+
+```
+
+---
+
+### E-Mail Templates (v9c)
+
+**1. Registrierung + E-Mail-Verifizierung:**
+```
+Betreff: Willkommen bei Mitai Jinkendo - E-Mail bestätigen
+
+Hallo {name},
+
+vielen Dank für deine Registrierung bei Mitai Jinkendo!
+
+Bitte bestätige deine E-Mail-Adresse:
+{app_url}/verify-email?token={token}
+
+Nach der Bestätigung startet dein 14-Tage Premium Trial automatisch.
+
+Viel Erfolg bei deinem Training!
+Dein Mitai Jinkendo Team
+```
+
+**2. Einladungslink (Admin):**
+```
+Betreff: Du wurdest zu Mitai Jinkendo eingeladen
+
+Hallo,
+
+{admin_name} hat dich zu Mitai Jinkendo eingeladen!
+
+Registriere dich jetzt:
+{app_url}/register?invite={token}
+
+Du erhältst {tier} Zugriff.
+
+Dein Mitai Jinkendo Team
+```
+
+---
+
+### Migrations-Reihenfolge (v9c)
+
+```
+Phase 1 - DB Schema:
+1. app_settings Tabelle + Initialdaten
+2. tiers Tabelle + 4 Standard-Tiers
+3. coupons Tabelle
+4. coupon_redemptions Tabelle
+5. access_grants Tabelle
+6. user_activity_log Tabelle
+7. user_stats Tabelle
+8. user_restrictions Tabelle
+9. profiles Spalten erweitern
+10. Bestehende Profile migrieren (Lars → tier='selfhosted', email_verified=true)
+
+Phase 2 - Backend:
+11. Tier-System Router + Middleware
+12. Registrierungs-Flow
+13. Coupon-System
+14. Access-Grant-Logik
+15. Activity-Logging
+16. Erweiterte Admin-Endpoints
+
+Phase 3 - Frontend:
+17. Registrierungs-Seiten
+18. Tier-System UI-Komponenten
+19. Coupon-Eingabe
+20. Erweiterte Admin-Panels
+21. Feature-Gates in bestehende Seiten einbauen
+
+Phase 4 - Cron-Jobs:
+22. Expired-Access-Checker
+23. Monthly-Reset
+24. Streak-Updater
+
+Phase 5 - Testing & Deployment:
+25. Dev-Testing
+26. Prod-Deployment
+```
+
+---
+
## Deployment
### Infrastruktur
diff --git a/backend/apply_v9c_migration.py b/backend/apply_v9c_migration.py
new file mode 100644
index 0000000..4a3bba5
--- /dev/null
+++ b/backend/apply_v9c_migration.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+"""
+Apply v9c Subscription System Migration
+
+This script checks if v9c migration is needed and applies it.
+Run automatically on container startup via main.py startup event.
+"""
+import os
+import psycopg2
+from psycopg2.extras import RealDictCursor
+
+
+def get_db_connection():
+ """Get PostgreSQL connection."""
+ return psycopg2.connect(
+ host=os.getenv("DB_HOST", "postgres"),
+ port=int(os.getenv("DB_PORT", 5432)),
+ database=os.getenv("DB_NAME", "mitai_prod"),
+ user=os.getenv("DB_USER", "mitai_prod"),
+ password=os.getenv("DB_PASSWORD", ""),
+ cursor_factory=RealDictCursor
+ )
+
+
+def migration_needed(conn):
+ """Check if v9c migration is needed."""
+ cur = conn.cursor()
+
+ # Check if tiers table exists
+ cur.execute("""
+ SELECT EXISTS (
+ SELECT FROM information_schema.tables
+ WHERE table_name = 'tiers'
+ )
+ """)
+ tiers_exists = cur.fetchone()['exists']
+
+ # Check if features table exists
+ cur.execute("""
+ SELECT EXISTS (
+ SELECT FROM information_schema.tables
+ WHERE table_name = 'features'
+ )
+ """)
+ features_exists = cur.fetchone()['exists']
+
+ cur.close()
+
+ # Migration needed if either table is missing
+ return not (tiers_exists and features_exists)
+
+
+def apply_migration():
+ """Apply v9c migration if needed."""
+ print("[v9c Migration] Checking if migration is needed...")
+
+ try:
+ conn = get_db_connection()
+
+ if not migration_needed(conn):
+ print("[v9c Migration] Already applied, skipping.")
+ conn.close()
+ return
+
+ print("[v9c Migration] Applying subscription system migration...")
+
+ # Read migration SQL
+ migration_path = os.path.join(
+ os.path.dirname(__file__),
+ "migrations",
+ "v9c_subscription_system.sql"
+ )
+
+ with open(migration_path, 'r', encoding='utf-8') as f:
+ migration_sql = f.read()
+
+ # Execute migration
+ cur = conn.cursor()
+ cur.execute(migration_sql)
+ conn.commit()
+ cur.close()
+ conn.close()
+
+ print("[v9c Migration] ✅ Migration completed successfully!")
+
+ # Verify tables created
+ conn = get_db_connection()
+ cur = conn.cursor()
+ cur.execute("""
+ SELECT table_name FROM information_schema.tables
+ WHERE table_schema = 'public'
+ AND table_name IN ('tiers', 'features', 'tier_limits', 'access_grants', 'coupons')
+ ORDER BY table_name
+ """)
+ tables = [r['table_name'] for r in cur.fetchall()]
+ print(f"[v9c Migration] Created tables: {', '.join(tables)}")
+
+ # Verify initial data
+ cur.execute("SELECT COUNT(*) as count FROM tiers")
+ tier_count = cur.fetchone()['count']
+ cur.execute("SELECT COUNT(*) as count FROM features")
+ feature_count = cur.fetchone()['count']
+ cur.execute("SELECT COUNT(*) as count FROM tier_limits")
+ limit_count = cur.fetchone()['count']
+
+ print(f"[v9c Migration] Initial data: {tier_count} tiers, {feature_count} features, {limit_count} tier limits")
+
+ cur.close()
+ conn.close()
+
+ except Exception as e:
+ print(f"[v9c Migration] ❌ Error: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ apply_migration()
diff --git a/backend/main.py b/backend/main.py
index 7d7249a..88204b3 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -51,6 +51,13 @@ async def startup_event():
print(f"⚠️ init_db() failed (non-fatal): {e}")
# Don't crash on startup - can be created manually
+ # Apply v9c migration if needed
+ try:
+ from apply_v9c_migration import apply_migration
+ apply_migration()
+ except Exception as e:
+ print(f"⚠️ v9c migration failed (non-fatal): {e}")
+
# ── Register Routers ──────────────────────────────────────────────────────────
app.include_router(auth.router) # /api/auth/*
app.include_router(profiles.router) # /api/profiles/*, /api/profile
@@ -71,4 +78,4 @@ app.include_router(importdata.router) # /api/import/*
@app.get("/")
def root():
"""API health check."""
- return {"status": "ok", "service": "mitai-jinkendo", "version": "v9b"}
+ return {"status": "ok", "service": "mitai-jinkendo", "version": "v9c-dev"}
diff --git a/backend/migrations/v9c_subscription_system.sql b/backend/migrations/v9c_subscription_system.sql
new file mode 100644
index 0000000..c5ff0fe
--- /dev/null
+++ b/backend/migrations/v9c_subscription_system.sql
@@ -0,0 +1,352 @@
+-- ============================================================================
+-- Mitai Jinkendo v9c: Subscription & Coupon System Migration
+-- ============================================================================
+-- Created: 2026-03-19
+-- Purpose: Add flexible tier system with Feature-Registry Pattern
+--
+-- Tables added:
+-- 1. app_settings - Global configuration
+-- 2. tiers - Subscription tiers (simplified)
+-- 3. features - Feature registry (all limitable features)
+-- 4. tier_limits - Tier x Feature matrix
+-- 5. user_feature_restrictions - Individual user overrides
+-- 6. user_feature_usage - Usage tracking
+-- 7. coupons - Coupon management
+-- 8. coupon_redemptions - Redemption history
+-- 9. access_grants - Time-limited access grants
+-- 10. user_activity_log - Activity tracking
+-- 11. user_stats - Aggregated statistics
+--
+-- Feature-Registry Pattern:
+-- Instead of hardcoded columns (max_weight_entries, max_ai_calls),
+-- all limits are defined in features table and configured via tier_limits.
+-- This allows adding new limitable features without schema changes.
+-- ============================================================================
+
+-- ============================================================================
+-- 1. app_settings - Global configuration
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS app_settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ description TEXT,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- ============================================================================
+-- 2. tiers - Subscription tiers (simplified)
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS tiers (
+ id TEXT PRIMARY KEY, -- 'free', 'basic', 'premium', 'selfhosted'
+ name TEXT NOT NULL, -- Display name
+ description TEXT, -- Marketing description
+ price_monthly_cents INTEGER, -- NULL for free/selfhosted
+ price_yearly_cents INTEGER, -- NULL for free/selfhosted
+ stripe_price_id_monthly TEXT, -- Stripe Price ID (for v9d)
+ stripe_price_id_yearly TEXT, -- Stripe Price ID (for v9d)
+ active BOOLEAN DEFAULT true, -- Can new users subscribe?
+ sort_order INTEGER DEFAULT 0,
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- ============================================================================
+-- 3. features - Feature registry (all limitable features)
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS features (
+ id TEXT PRIMARY KEY, -- 'weight_entries', 'ai_calls', 'photos', etc.
+ name TEXT NOT NULL, -- Display name
+ description TEXT, -- What is this feature?
+ category TEXT, -- 'data', 'ai', 'export', 'integration'
+ limit_type TEXT DEFAULT 'count', -- 'count', 'boolean', 'quota'
+ reset_period TEXT DEFAULT 'never', -- 'never', 'monthly', 'daily'
+ default_limit INTEGER, -- Fallback if no tier_limit defined
+ active BOOLEAN DEFAULT true, -- Is this feature currently used?
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- ============================================================================
+-- 4. tier_limits - Tier x Feature matrix
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS tier_limits (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ tier_id TEXT NOT NULL REFERENCES tiers(id) ON DELETE CASCADE,
+ feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
+ limit_value INTEGER, -- NULL = unlimited, 0 = disabled
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(tier_id, feature_id)
+);
+
+-- ============================================================================
+-- 5. user_feature_restrictions - Individual user overrides
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS user_feature_restrictions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
+ limit_value INTEGER, -- NULL = unlimited, 0 = disabled
+ reason TEXT, -- Why was this override applied?
+ created_by UUID, -- Admin profile_id
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(profile_id, feature_id)
+);
+
+-- ============================================================================
+-- 6. user_feature_usage - Usage tracking
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS user_feature_usage (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
+ usage_count INTEGER DEFAULT 0,
+ reset_at TIMESTAMP, -- When does this counter reset?
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(profile_id, feature_id)
+);
+
+-- ============================================================================
+-- 7. coupons - Coupon management
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS coupons (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ code TEXT UNIQUE NOT NULL,
+ type TEXT NOT NULL, -- 'single_use', 'period', 'wellpass'
+ tier_id TEXT REFERENCES tiers(id) ON DELETE SET NULL,
+ duration_days INTEGER, -- For period/wellpass coupons
+ max_redemptions INTEGER, -- NULL = unlimited
+ redemption_count INTEGER DEFAULT 0,
+ valid_from TIMESTAMP,
+ valid_until TIMESTAMP,
+ active BOOLEAN DEFAULT true,
+ created_by UUID, -- Admin profile_id
+ description TEXT, -- Internal note
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- ============================================================================
+-- 8. coupon_redemptions - Redemption history
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS coupon_redemptions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ coupon_id UUID NOT NULL REFERENCES coupons(id) ON DELETE CASCADE,
+ profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ redeemed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ access_grant_id UUID, -- FK to access_grants (created as result)
+ UNIQUE(coupon_id, profile_id) -- One redemption per user per coupon
+);
+
+-- ============================================================================
+-- 9. access_grants - Time-limited access grants
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS access_grants (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ tier_id TEXT NOT NULL REFERENCES tiers(id) ON DELETE CASCADE,
+ granted_by TEXT, -- 'coupon', 'admin', 'trial', 'subscription'
+ coupon_id UUID REFERENCES coupons(id) ON DELETE SET NULL,
+ valid_from TIMESTAMP NOT NULL,
+ valid_until TIMESTAMP NOT NULL,
+ is_active BOOLEAN DEFAULT true, -- Can be paused by Wellpass logic
+ paused_by UUID, -- access_grant.id that paused this
+ paused_at TIMESTAMP, -- When was it paused?
+ remaining_days INTEGER, -- Days left when paused (for resume)
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- ============================================================================
+-- 10. user_activity_log - Activity tracking
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS user_activity_log (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ action TEXT NOT NULL, -- 'login', 'logout', 'coupon_redeemed', 'tier_changed'
+ details JSONB, -- Flexible metadata
+ ip_address TEXT,
+ user_agent TEXT,
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX IF NOT EXISTS idx_activity_log_profile ON user_activity_log(profile_id, created DESC);
+CREATE INDEX IF NOT EXISTS idx_activity_log_action ON user_activity_log(action, created DESC);
+
+-- ============================================================================
+-- 11. user_stats - Aggregated statistics
+-- ============================================================================
+CREATE TABLE IF NOT EXISTS user_stats (
+ profile_id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
+ last_login TIMESTAMP,
+ login_count INTEGER DEFAULT 0,
+ weight_entries_count INTEGER DEFAULT 0,
+ ai_calls_count INTEGER DEFAULT 0,
+ photos_count INTEGER DEFAULT 0,
+ total_data_points INTEGER DEFAULT 0,
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- ============================================================================
+-- Extend profiles table with subscription fields
+-- ============================================================================
+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS tier TEXT DEFAULT 'free';
+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS trial_ends_at TIMESTAMP;
+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT false;
+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email_verify_token TEXT;
+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS invited_by UUID REFERENCES profiles(id) ON DELETE SET NULL;
+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS invitation_token TEXT;
+
+-- ============================================================================
+-- Insert initial data
+-- ============================================================================
+
+-- App settings
+INSERT INTO app_settings (key, value, description) VALUES
+ ('trial_duration_days', '14', 'Default trial duration for new registrations'),
+ ('post_trial_tier', 'free', 'Tier after trial expires (free/disabled)'),
+ ('require_email_verification', 'true', 'Require email verification before activation'),
+ ('self_registration_enabled', 'true', 'Allow self-registration')
+ON CONFLICT (key) DO NOTHING;
+
+-- Tiers
+INSERT INTO tiers (id, name, description, price_monthly_cents, price_yearly_cents, active, sort_order) VALUES
+ ('free', 'Free', 'Eingeschränkte Basis-Funktionen', NULL, NULL, true, 1),
+ ('basic', 'Basic', 'Kernfunktionen ohne KI', 499, 4990, true, 2),
+ ('premium', 'Premium', 'Alle Features inkl. KI und Connectoren', 999, 9990, true, 3),
+ ('selfhosted', 'Self-Hosted', 'Unbegrenzt (für Heimserver)', NULL, NULL, false, 4)
+ON CONFLICT (id) DO NOTHING;
+
+-- Features (11 initial features)
+INSERT INTO features (id, name, description, category, limit_type, reset_period, default_limit, active) VALUES
+ ('weight_entries', 'Gewichtseinträge', 'Anzahl Gewichtsmessungen', 'data', 'count', 'never', NULL, true),
+ ('circumference_entries', 'Umfangs-Einträge', 'Anzahl Umfangsmessungen', 'data', 'count', 'never', NULL, true),
+ ('caliper_entries', 'Caliper-Einträge', 'Anzahl Hautfaltenmessungen', 'data', 'count', 'never', NULL, true),
+ ('nutrition_entries', 'Ernährungs-Einträge', 'Anzahl Ernährungslogs', 'data', 'count', 'never', NULL, true),
+ ('activity_entries', 'Aktivitäts-Einträge', 'Anzahl Trainings/Aktivitäten', 'data', 'count', 'never', NULL, true),
+ ('photos', 'Progress-Fotos', 'Anzahl hochgeladene Fotos', 'data', 'count', 'never', NULL, true),
+ ('ai_calls', 'KI-Analysen', 'KI-Auswertungen pro Monat', 'ai', 'count', 'monthly', 0, true),
+ ('ai_pipeline', 'KI-Pipeline', 'Vollständige Pipeline-Analyse', 'ai', 'boolean', 'never', 0, true),
+ ('export_csv', 'CSV-Export', 'Daten als CSV exportieren', 'export', 'boolean', 'never', 0, true),
+ ('export_json', 'JSON-Export', 'Daten als JSON exportieren', 'export', 'boolean', 'never', 0, true),
+ ('export_zip', 'ZIP-Export', 'Vollständiger Backup-Export', 'export', 'boolean', 'never', 0, true)
+ON CONFLICT (id) DO NOTHING;
+
+-- Tier x Feature Matrix (tier_limits)
+-- Format: (tier, feature, limit) - NULL = unlimited, 0 = disabled
+
+-- FREE tier (sehr eingeschränkt)
+INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
+ ('free', 'weight_entries', 30),
+ ('free', 'circumference_entries', 10),
+ ('free', 'caliper_entries', 10),
+ ('free', 'nutrition_entries', 30),
+ ('free', 'activity_entries', 30),
+ ('free', 'photos', 5),
+ ('free', 'ai_calls', 0), -- Keine KI
+ ('free', 'ai_pipeline', 0), -- Keine Pipeline
+ ('free', 'export_csv', 0), -- Kein Export
+ ('free', 'export_json', 0),
+ ('free', 'export_zip', 0)
+ON CONFLICT (tier_id, feature_id) DO NOTHING;
+
+-- BASIC tier (Kernfunktionen)
+INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
+ ('basic', 'weight_entries', NULL), -- Unbegrenzt
+ ('basic', 'circumference_entries', NULL),
+ ('basic', 'caliper_entries', NULL),
+ ('basic', 'nutrition_entries', NULL),
+ ('basic', 'activity_entries', NULL),
+ ('basic', 'photos', 50),
+ ('basic', 'ai_calls', 3), -- 3 KI-Calls/Monat
+ ('basic', 'ai_pipeline', 0), -- Keine Pipeline
+ ('basic', 'export_csv', 1), -- Export erlaubt
+ ('basic', 'export_json', 1),
+ ('basic', 'export_zip', 1)
+ON CONFLICT (tier_id, feature_id) DO NOTHING;
+
+-- PREMIUM tier (alles unbegrenzt)
+INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
+ ('premium', 'weight_entries', NULL),
+ ('premium', 'circumference_entries', NULL),
+ ('premium', 'caliper_entries', NULL),
+ ('premium', 'nutrition_entries', NULL),
+ ('premium', 'activity_entries', NULL),
+ ('premium', 'photos', NULL),
+ ('premium', 'ai_calls', NULL), -- Unbegrenzt KI
+ ('premium', 'ai_pipeline', 1), -- Pipeline erlaubt
+ ('premium', 'export_csv', 1),
+ ('premium', 'export_json', 1),
+ ('premium', 'export_zip', 1)
+ON CONFLICT (tier_id, feature_id) DO NOTHING;
+
+-- SELFHOSTED tier (alles unbegrenzt)
+INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
+ ('selfhosted', 'weight_entries', NULL),
+ ('selfhosted', 'circumference_entries', NULL),
+ ('selfhosted', 'caliper_entries', NULL),
+ ('selfhosted', 'nutrition_entries', NULL),
+ ('selfhosted', 'activity_entries', NULL),
+ ('selfhosted', 'photos', NULL),
+ ('selfhosted', 'ai_calls', NULL),
+ ('selfhosted', 'ai_pipeline', 1),
+ ('selfhosted', 'export_csv', 1),
+ ('selfhosted', 'export_json', 1),
+ ('selfhosted', 'export_zip', 1)
+ON CONFLICT (tier_id, feature_id) DO NOTHING;
+
+-- ============================================================================
+-- Migrate existing profiles
+-- ============================================================================
+
+-- Lars' Profile → selfhosted tier with email verified
+UPDATE profiles
+SET
+ tier = 'selfhosted',
+ email_verified = true
+WHERE
+ email = 'lars@stommer.com'
+ OR role = 'admin';
+
+-- Other existing profiles → free tier, unverified
+UPDATE profiles
+SET
+ tier = 'free',
+ email_verified = false
+WHERE
+ tier IS NULL
+ OR tier = '';
+
+-- Initialize user_stats for existing profiles
+INSERT INTO user_stats (profile_id, weight_entries_count, photos_count)
+SELECT
+ p.id,
+ (SELECT COUNT(*) FROM weight_log WHERE profile_id = p.id),
+ (SELECT COUNT(*) FROM photos WHERE profile_id = p.id)
+FROM profiles p
+ON CONFLICT (profile_id) DO NOTHING;
+
+-- ============================================================================
+-- Create indexes for performance
+-- ============================================================================
+CREATE INDEX IF NOT EXISTS idx_tier_limits_tier ON tier_limits(tier_id);
+CREATE INDEX IF NOT EXISTS idx_tier_limits_feature ON tier_limits(feature_id);
+CREATE INDEX IF NOT EXISTS idx_user_restrictions_profile ON user_feature_restrictions(profile_id);
+CREATE INDEX IF NOT EXISTS idx_user_usage_profile ON user_feature_usage(profile_id);
+CREATE INDEX IF NOT EXISTS idx_access_grants_profile ON access_grants(profile_id, valid_until DESC);
+CREATE INDEX IF NOT EXISTS idx_access_grants_active ON access_grants(profile_id, is_active, valid_until DESC);
+CREATE INDEX IF NOT EXISTS idx_coupons_code ON coupons(code);
+CREATE INDEX IF NOT EXISTS idx_coupon_redemptions_profile ON coupon_redemptions(profile_id);
+
+-- ============================================================================
+-- Migration complete
+-- ============================================================================
+-- Run this migration with:
+-- psql -h localhost -U mitai_prod -d mitai_prod < backend/migrations/v9c_subscription_system.sql
+--
+-- Or via Docker:
+-- docker exec -i mitai-postgres psql -U mitai_prod -d mitai_prod < backend/migrations/v9c_subscription_system.sql
+-- ============================================================================