- Enhanced the ACCESS_LAYER_AND_GOVERNANCE_PLAN.md with new specifications for capability documentation and community features. - Added references to new documents detailing capability IDs and club membership features. - Updated MULTI_TENANCY_RBAC_ARCHITECTURE.md to include links to the new specifications. - Marked certain features as deprecated in backend/auth.py, indicating migration paths for club feature access. - Incremented DB_SCHEMA_VERSION to 20260606078 in version.py to reflect recent changes.
17 KiB
Vereins-Membership & Feature-System Shinkan v1
Status: Konzept (Schema- und Enforcement-Zielbild vor Implementierung)
Stand: 2026-06-06
Bezüge: Schwesterprojekt Mitai (v9c_subscription_system.sql, FEATURE_ENFORCEMENT.md), CAPABILITY_CATALOG.v1.md, MULTI_TENANCY_RBAC_ARCHITECTURE.md §4.6, ACCESS_LAYER_AND_GOVERNANCE_PLAN.md
1. Zweck
Shinkan verkauft und limitiert nicht Einzelpersonen (wie Mitai), sondern Vereine. Dieses Dokument definiert:
- das Feature-Registry-Muster (limitierbare Funktionen),
- das Vereins-Abo (
club_plans,club_subscriptions), - Kontingente und Enforcement,
- die Abbildung von Mitai und Vermeidung von Refactoring-Schulden.
Capabilities (Rollen: darf ich die Funktion?) → CAPABILITY_CATALOG.v1.md.
2. Grundprinzip: Zwei Achsen
flowchart TB
subgraph cap [Achse 1 — Capabilities]
CR[club_role_capability_grants]
PR[portal_role_capability_grants]
end
subgraph feat [Achse 2 — Features / Kontingente]
FP[club_plans]
FPL[club_plan_limits]
FS[club_subscriptions]
FU[club_feature_usage]
end
subgraph gov [Achse 3 — Governance]
GV[visibility / club_id / created_by]
end
REQ[HTTP Request] --> ACCT[Account-Lifecycle]
ACCT --> cap
cap --> gov
gov --> feat
feat --> EXEC[Ausführung + increment]
| Frage | System | Subjekt |
|---|---|---|
| Darf Trainer X KI nutzen? | Capability exercises.ai.suggest |
profile_id + club_role |
| Wie viele KI-Aufrufe hat Verein Y? | Feature ai_calls |
club_id |
| Darf ich diese Übung ändern? | Governance | Objekt + Mitgliedschaft |
Beide Achsen müssen erfüllt sein (AND), außer dokumentierte Plattform-Ausnahmen.
3. Mitai-Mapping (was übernehmen, was nicht)
3.1 Übernehmen (Pattern)
| Mitai (Person) | Shinkan (Verein) | Anmerkung |
|---|---|---|
features (TEXT-PK, Registry) |
features (app='shinkan') |
Gemeinsames Muster, ggf. später Jinkendo-weit |
tiers |
club_plans |
Produktdefinition |
tier_limits |
club_plan_limits |
Matrix Plan × Feature |
user_feature_restrictions |
club_feature_overrides |
Admin-Override pro Verein |
user_feature_usage |
club_feature_usage |
Verbrauch pro Verein |
access_grants |
club_access_grants |
Trial, Promo, manuelle Freischaltung |
check_feature_access() |
check_club_feature_access() |
Subjekt club_id |
increment_feature_usage() |
increment_club_feature_usage() |
Nur bei INSERT / KI-Call |
| 4-Phasen-Rollout | identisch | Log → UI → Hard-Block |
GET /api/features/usage |
GET /api/clubs/{id}/entitlements |
siehe Capability-Doc §7 |
3.2 Nicht übernehmen
| Mitai | Shinkan-Grund |
|---|---|
profiles.tier als Haupt-Abo |
Verein zahlt, nicht Einzeltrainer |
subscriptions (Shinkan 001, INT-Features) |
Ungenutzt, Schema-Drift |
get_effective_tier(profile_id) für Shinkan-Limits |
Ersetzen durch get_effective_club_plan(club_id) |
| Profil-zentrierte Enforcement-Hooks allein | Primär club_id; Profil nur für Attribution |
3.3 Parallelität Jinkendo-Familie (später)
CENTRAL_SUBSCRIPTION_SYSTEM.md (Mitai): zentrales Personen-Abo über Apps.
Zielbild ohne Refactoring:
features.enforcement_subject ∈ { 'club', 'profile', 'portal' }
effektives_limit(feature) = merge(
club_plan_limit(club_id, feature), # Shinkan-Hauptquelle
profile_grant_limit(profile_id, feature) # optional Jinkendo-Bonus
)
Merge-Regel (Vorschlag): Maximum der erlaubten Kontingente, boolean = OR. Details vor Stripe festlegen.
4. Ist-Zustand Shinkan (Drift — zuerst bereinigen)
| Artefakt | Problem |
|---|---|
backend/migrations/001_auth_membership.sql |
features.id SERIAL, tier_limits.tier VARCHAR |
backend/auth.py check_feature_access() |
Erwartet Mitai-v9c-Schema (features.id TEXT, tier_id, limit_type, …) |
| Kein Router | Ruft check_feature_access auf |
profiles.tier |
Existiert, ohne Shinkan-Enforcement |
Pflicht vor Phase 3 (Enforcement): Migration 0XX_club_features_v1.sql — v9c-kompatibles Feature-Schema + Vereins-Tabellen; alte 001-Feature-Zeilen migrieren oder deprecaten.
5. Ziel-Schema (v1)
5.1 Feature-Registry (app-weit, Mitai-kompatibel)
-- Konzept — Implementierung als nummerierte Migration
CREATE TABLE features (
id TEXT PRIMARY KEY, -- z.B. 'ai_calls'
app TEXT NOT NULL DEFAULT 'shinkan',
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL, -- 'content'|'planning'|'ai'|'org'|'integration'|'platform'
limit_type TEXT NOT NULL DEFAULT 'count', -- 'count' | 'boolean'
reset_period TEXT NOT NULL DEFAULT 'never', -- 'never' | 'daily' | 'monthly'
default_limit INTEGER, -- NULL=∞, 0=aus
enforcement_subject TEXT NOT NULL DEFAULT 'club', -- 'club'|'profile'|'portal'
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
5.2 Vereins-Produkte & Abo
CREATE TABLE club_plans (
id TEXT PRIMARY KEY, -- 'free', 'verein_starter', 'verein_pro'
name TEXT NOT NULL,
description TEXT,
price_monthly_cents INTEGER,
price_yearly_cents INTEGER,
stripe_price_id_monthly TEXT,
stripe_price_id_yearly TEXT,
active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE club_subscriptions (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT NOT NULL REFERENCES club_plans(id),
status TEXT NOT NULL DEFAULT 'active', -- active|trial|past_due|cancelled
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ends_at TIMESTAMPTZ,
trial_ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (club_id) -- ein aktiver Plan pro Verein (v1)
);
CREATE TABLE club_plan_limits (
id SERIAL PRIMARY KEY,
plan_id TEXT NOT NULL REFERENCES club_plans(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER, -- NULL=∞, 0=deaktiviert
UNIQUE (plan_id, feature_id)
);
5.3 Overrides, Grants, Verbrauch
CREATE TABLE club_feature_overrides (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER NOT NULL,
reason TEXT,
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (club_id, feature_id)
);
CREATE TABLE club_access_grants (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT REFERENCES club_plans(id),
feature_id TEXT REFERENCES features(id), -- optional Einzel-Feature
grant_limit INTEGER,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
reason TEXT,
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL
);
CREATE TABLE club_feature_usage (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
usage_count INTEGER NOT NULL DEFAULT 0,
reset_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
UNIQUE (club_id, feature_id)
);
-- Optional: Attribution / Fairness / Audit
CREATE TABLE club_feature_usage_events (
id BIGSERIAL PRIMARY KEY,
club_id INT NOT NULL,
feature_id TEXT NOT NULL,
profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
action TEXT NOT NULL, -- 'ai_suggest', 'exercise_create', ...
created_at TIMESTAMPTZ DEFAULT NOW()
);
5.4 Capabilities (Rollen — Kurzreferenz)
Siehe CAPABILITY_CATALOG.v1.md für IDs. Tabellen:
CREATE TABLE capabilities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
domain TEXT NOT NULL,
min_account_state TEXT NOT NULL DEFAULT 'active_member',
linked_feature_id TEXT REFERENCES features(id), -- optional Kontingent
active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE club_role_capability_grants (
role_code TEXT NOT NULL, -- club_admin, trainer, ...
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (role_code, capability_id)
);
CREATE TABLE portal_role_capability_grants (
portal_role TEXT NOT NULL, -- admin, superadmin
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (portal_role, capability_id)
);
6. Shinkan Feature-Katalog (Seed v1)
Übernahme aus 001_auth_membership.sql + Ist-Endpoints, angereichert:
| feature_id | category | limit_type | reset_period | enforcement_subject | Default Free | Beschreibung |
|---|---|---|---|---|---|---|
exercises |
content | count | never | club | 100 | Anzahl Übungen im Verein (Bestand) |
exercise_media |
content | count | monthly | club | 20 | Medien-Uploads / Monat |
training_units |
planning | count | monthly | club | 40 | Geplante/durchgeführte Einheiten |
training_programs |
planning | count | never | club | 5 | Module + Rahmenprogramme (kombiniert v1) |
training_groups |
org | count | never | club | 10 | Trainingsgruppen |
active_members |
org | count | never | club | 25 | Aktive Mitglieder |
ai_calls |
ai | count | monthly | club | 0 | KI-Aufrufe (Suggest, Regenerate, Planung) |
ai_pipeline |
ai | boolean | never | club | 0 | Erweiterte KI-Pipelines (Batch, später) |
wiki_import |
integration | boolean | never | portal | 0 | MediaWiki-Import (Superadmin) |
data_export |
integration | boolean | never | club | 0 | Export-Funktionen (wenn eingeführt) |
Hinweis: Free-Defaults sind Produktentscheidung — Tabelle dient Implementierung.
6.1 Beispiel-Pläne (Seed)
| plan_id | ai_calls/Monat | exercises | active_members |
|---|---|---|---|
free |
0 | 100 | 25 |
verein_starter |
30 | 500 | 80 |
verein_pro |
200 | NULL (∞) | NULL |
pilot |
100 | NULL | NULL |
Jeder Verein erhält bei Anlage durch Superadmin initial club_subscriptions.plan_id = 'free' (oder pilot).
7. Auflösungslogik
7.1 Effektiver Vereinsplan
def get_effective_club_plan(cur, club_id: int) -> str:
"""
1. Aktiver club_access_grants mit plan_id (höchste Priorität, Zeitfenster)
2. club_subscriptions.status == 'active' → plan_id
3. Fallback 'free'
"""
7.2 Feature-Limit (analog Mitai check_feature_access)
def check_club_feature_access(
cur,
club_id: int,
feature_id: str,
*,
profile_id: int | None = None, # nur für Logging / optionale Profil-Boni später
) -> dict:
"""
Priorität:
1. club_feature_overrides (club_id, feature_id)
2. club_plan_limits für get_effective_club_plan(club_id)
3. features.default_limit
Auswertung:
- limit_type boolean: limit_value == 1
- limit_type count: used < limit (club_feature_usage, reset beachten)
Returns: { allowed, limit, used, remaining, reason, reset_at }
"""
7.3 Vollständige Request-Kette
1. require_auth
2. assert_account_state(min_state) # unverified / verified_pending_club / active_member
3. get_tenant_context
4. assert_capability(tenant, cap_id) # Rollen-Achse
5. assert_content_governance(...) # nur bei Objekt-Endpoints
6. check_club_feature_access(club_id, feature_id)
7. … Business-Logik …
8. increment_club_feature_usage(club_id, feature_id) # nur bei INSERT / KI-Execute
9. optional: log club_feature_usage_events (profile_id)
7.4 Wer zählt als Verbrauch?
| Aktion | increment | Subjekt |
|---|---|---|
POST /exercises (neu) |
exercises |
club_id des Objekts oder effective_club_id |
| Medien-Upload | exercise_media |
Verein des Mediums |
| KI Suggest/Regenerate | ai_calls |
effective_club_id |
| Mitglied hinzufügen | active_members |
Ziel-club_id |
| Trainingsgruppe anlegen | training_groups |
club_id |
Mitai-Regel: Counter nicht bei UPDATE/DELETE erhöhen.
8. API-Oberfläche
8.1 Nutzer / Vereinsadmin
GET /api/clubs/{club_id}/entitlements
Kombiniert Capabilities + Feature-Kontingente (siehe CAPABILITY_CATALOG.v1.md §7.1).
GET /api/me/entitlements?club_id=12
Bequemer Alias für aktiven Verein.
8.2 Superadmin / Plattform
| Endpoint | Zweck |
|---|---|
GET/PUT /api/admin/club-plans |
Plan-CRUD |
GET/PUT /api/admin/club-plan-limits |
Matrix |
GET/PUT /api/admin/clubs/{id}/subscription |
Verein-Abo |
GET/PUT /api/admin/clubs/{id}/feature-overrides |
Sonderkontingente |
POST /api/admin/clubs/{id}/access-grants |
Trial/Promo |
Vorbild UI: Mitai AdminTierLimitsPage.jsx, AdminUserRestrictionsPage.jsx → Vereins-Kontext.
8.3 Geplant: Vereinsgründung
POST /api/club-creation-requests # Nutzer (verified_pending_club)
GET /api/admin/club-creation-requests
POST /api/admin/club-creation-requests/{id}/approve # legt club + subscription an
9. Vier-Phasen-Rollout (aus Mitai)
| Phase | Shinkan-Aktivität | Nutzer sichtbar? |
|---|---|---|
| 0 | Schema-Migration, Seed features + club_plans, Drift 001 bereinigen |
Nein |
| 1 | Account-Gates + Capability-Grants (ohne Limits) | Onboarding-Hinweise |
| 2 | check_club_feature_access — nur JSON-Log (feature_logger analog Mitai) |
Nein |
| 3 | GET …/entitlements + UsageBadge im UI |
Ja (Kontingent-Anzeige) |
| 4 | HTTP 403 bei Limit + increment |
Ja (Hard-Block) |
Reihenfolge innerhalb Phase 4: zuerst ai_calls, dann exercise_media, dann Bestands-Limits (exercises, active_members).
10. CI / Test-Isolation (Betrieb)
Unabhängig vom Membership-System — Pflicht wegen Prod-Vorfälle (access_layer_it_*@test.local):
| Regel | Umsetzung |
|---|---|
| Integrationstests nie gegen Prod-DB | Eigene Test-DB oder Job-Postgres in Gitea |
ENVIRONMENT=production + ALLOW_INTEGRATION_TESTS |
Default 0, Tests abbrechen |
| Test-Accounts | E-Mail @test.local oder profiles.is_test_account |
| Cleanup | Fixture-finally + Nightly-Job löscht Leichen |
.gitea/workflows/test.yml: pytest-backend gegen Deploy-DB ersetzen durch isolierte DB (eigenes Epic, parallel zu Membership).
11. Implementierungs-Roadmap (gesamt)
| Schritt | Deliverable | Membership-relevant |
|---|---|---|
| M0 | CI-Isolation + Prod-Cleanup-Runbook | Nein |
| M1 | Migration Feature-Schema v9c + club_plans/club_subscriptions (leer nutzbar) |
Ja |
| M2 | check_club_feature_access + Seed Pläne |
Ja |
| M3 | Account-Lifecycle + Capability-Grants | Capabilities |
| M4 | GET /me/entitlements |
Ja |
| M5 | Enforcement ai_calls (Phase 4) |
Ja |
| M6 | Admin Plan-Matrix UI | Ja |
| M7 | club_creation_requests |
Prozess |
| M8 | Stripe / Rechnung | Später |
12. Offene Produktentscheidungen
Vor M6 festlegen:
- Zählen
active_members: alle Mitglieder oder nur Rollen mit Planungsrecht? - Soft-Limit vs. Hard-Stop: Warnung bei 80 % oder sofort 403?
- Pilotverein: eigener Plan
pilotmit hohen Limits? - KI-Fairness: nur Vereinslimit oder zusätzlich Max pro Trainer/Monat?
- Offizielle Inhalte: für
verified_pending_clubsichtbar oder gesperrt? (Capability-Doc) - Portal
adminvs.superadmin: Wer darf Vereine anlegen? (Ziel: nursuperadminfür Freigabe)
13. Referenzen
| Pfad | Inhalt |
|---|---|
c:/dev/mitai-jinkendo/backend/migrations/v9c_subscription_system.sql |
Mitai-Schema-Vorlage |
c:/dev/mitai-jinkendo/.claude/docs/architecture/FEATURE_ENFORCEMENT.md |
4-Phasen-Modell |
c:/dev/mitai-jinkendo/.claude/docs/technical/MEMBERSHIP_SYSTEM.md |
Mitai-Hauptdoku |
c:/dev/mitai-jinkendo/.claude/docs/technical/CENTRAL_SUBSCRIPTION_SYSTEM.md |
Jinkendo-Familie später |
CAPABILITY_CATALOG.v1.md |
Rollen & Capabilities |
MULTI_TENANCY_RBAC_ARCHITECTURE.md §4.6 |
Ursprüngliches Vereinsabo-Zielbild |
ACCESS_LAYER_AND_GOVERNANCE_PLAN.md |
Stufe E/F |
Changelog
- 2026-06-06: v1 — Mitai-Mapping, Ziel-Schema, Feature-Seed, Auflösungslogik, Rollout.