mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
First module to consume the scope layer — proves the model end-to-end
on a real query path.
Changes in calendar/queries.ts:
- db.table('calendars') → scopedForModule<LocalCalendar>('calendar', 'calendars')
- db.table('timeBlocks') → scopedForModule<LocalTimeBlock>('calendar', 'timeBlocks')
- db.table('events') → scopedForModule<LocalEvent>('calendar', 'events')
- applyVisibility() wrapper runs on each read to drop private records
authored by other members of a shared space.
Scope wrapper tweaks:
- getInScopeSpaceIds is now lenient during boot: if no active space has
loaded yet, falls back to the user's personal sentinel so sentinel-
stamped records from the v28 migration still render. Returns [] only
when fully unauthenticated, which yields an empty-match filter.
- applyVisibility is no longer generic-constrained — T is inferred
exactly as the input type; visibility/authorId are read via runtime
duck-typing so arbitrary record shapes pass through cleanly.
Known follow-ups:
- Root-layout bootstrap (load active space + reconcile sentinels on
login) is intentionally not wired up yet — needs a separate pass on
the already-crowded (app) layout to avoid collateral damage.
- Four legacy tables (conversations, documents, spaceMembers,
memoSpaces) carry a pre-existing `spaceId` field that points to the
older context-space concept, not our multi-tenancy space. Renaming
those to contextSpaceId is a tracked follow-up in the RFC — calendar
is unaffected.
Plan: docs/plans/spaces-foundation.md (updated with the legacy-spaceId
note + lenient-scope rationale).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
362 lines
18 KiB
Markdown
362 lines
18 KiB
Markdown
# Spaces — Foundation Plan
|
||
|
||
## Status (2026-04-20)
|
||
|
||
**RFC / Architektur-Entscheidung, Code noch nicht begonnen.** Dieses Dokument fixiert den Umbau des Scope-Modells *bevor* Prod-Daten existieren. Voraussetzung für: Social Relay, `clubs/`-Paket der ClubDesk-Roadmap, Team-/Brand-/Familien-Features generell.
|
||
|
||
**Begriff gewählt:** `Space`. Nicht `Workspace` (Slack/Notion-Assoziation zu stark), nicht `Context` (kollidiert mit `kontext/`-Modul), nicht `Org` (impliziert juristische Entität, passt nicht für Familie/Personal).
|
||
|
||
---
|
||
|
||
## Ziel
|
||
|
||
Den User-zentrierten Daten-Scope ("jeder Record gehört implizit dem eingeloggten User") durch einen **Space-zentrierten** Scope ersetzen: jeder Record gehört einem Space, der User ist Mitglied eines oder mehrerer Spaces.
|
||
|
||
**Kernfrage:** *"In welchem Kontext arbeite ich gerade, und wer sieht/darf was?"* — einheitlich beantwortet für Solo-User, Familie, Verein, Brand, Team.
|
||
|
||
Damit fällt die Polymorphie `User vs. Org` weg, die wir in der Scope-Debatte als Hauptkomplexitätstreiber identifiziert haben. Industriestandard (Notion, Linear, Slack, Figma) aus gutem Grund.
|
||
|
||
---
|
||
|
||
## Leitprinzipien
|
||
|
||
1. **Eine Primitive: `Space`.** Nie User vs. Org. User hat von Anfang an mindestens einen Space (`personal`), kann weitere haben.
|
||
2. **Jeder Record hat `spaceId`. Keine Ausnahmen.** Auch Mood-Einträge, Schlafdaten, Träume leben im Personal-Space.
|
||
3. **Ohne aktiven Space keine Query.** Der Scope-Wrapper erzwingt das strukturell.
|
||
4. **Better Auth Organizations ist die Identity-Layer.** Wir bauen auf dem Plugin, nicht daneben.
|
||
5. **Privat-innerhalb-Space via `visibility`-Flag**, nicht über Scope-Polymorphie.
|
||
6. **Space-Typ bestimmt Modul-Sichtbarkeit.** Ein Brand-Space hat kein `meditate`-Modul, ein Personal-Space kein `club-finance`.
|
||
|
||
---
|
||
|
||
## Die Primitive: Space
|
||
|
||
Ein `Space` ist eine Better-Auth-Organization mit `metadata.type`.
|
||
|
||
```ts
|
||
type SpaceType =
|
||
| 'personal' // single-member, vom User selbst, existiert automatisch
|
||
| 'brand' // externe Kommunikations-Identität (Edisconet, Howspace)
|
||
| 'club' // Verein (ClubDesk-Ziel)
|
||
| 'family' // WG / Familie
|
||
| 'team' // Arbeitsteam / Projekt
|
||
| 'practice' // Freelancer-Praxis / Selbstständigkeit
|
||
|
||
type Space = {
|
||
id: string
|
||
slug: string // @edisconet, @mein-verein, @me
|
||
name: string
|
||
logo: string | null
|
||
type: SpaceType
|
||
createdAt: number
|
||
metadata: {
|
||
// Typ-spezifische Zusatzfelder — schwach typisiert, modul-spezifisch
|
||
voiceDoc?: string // brand/club
|
||
legalEntity?: string // brand/club/practice
|
||
uid?: string // brand/club/practice: MwSt / UID
|
||
aiPersonaId?: string // optional: 1:1 zu ai-agents-Eintrag
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
### Space-Typ → Modul-Allowlist
|
||
|
||
Jeder Space-Typ schaltet einen Subset der 120+ Module frei. Nicht konfigurierbar pro Space in Phase 1 — erst wenn Bedarf auftaucht.
|
||
|
||
```ts
|
||
// packages/shared-branding/src/space-modules.ts
|
||
const SPACE_MODULES: Record<SpaceType, readonly ModuleId[]> = {
|
||
personal: [ /* alle 120+ */ ],
|
||
brand: ['social-relay', 'mail', 'landing', 'contacts', 'calendar', 'files', 'tasks', 'ai-agents', 'profile'],
|
||
club: ['club-members', 'club-finance', 'calendar', 'events', 'mail', 'landing', 'files', 'tasks', 'profile'],
|
||
family: ['calendar', 'events', 'shopping', 'recipes', 'files', 'tasks', 'finance-shared', 'profile'],
|
||
team: ['tasks', 'calendar', 'files', 'mail', 'chat', 'ai-agents', 'profile'],
|
||
practice: ['invoicing', 'contacts', 'calendar', 'mail', 'finance', 'files', 'tasks', 'profile'],
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Data Model
|
||
|
||
### Jede Tabelle, jeder Record
|
||
|
||
```ts
|
||
// alle Dexie-Tabellen + alle Postgres-Tabellen
|
||
spaceId: string // → organizations.id, NOT NULL, indexed
|
||
visibility: 'space' | 'private' // default: 'space'
|
||
authorId: string // NOT NULL, wer den Record erstellt hat
|
||
```
|
||
|
||
- **`spaceId`**: Scope-Partition. Query startet hier.
|
||
- **`visibility: 'private'`**: Record sichtbar nur für `authorId`, auch in Multi-Member-Spaces. Ersetzt den User-vs-Org-Gedanken für den Edge-Fall "persönlicher Draft in geteiltem Space".
|
||
- **`authorId`**: unabhängig vom Space-Owner — wichtig für Multi-Member-Audit ("wer hat den Eintrag angelegt").
|
||
|
||
### Better-Auth-Erweiterung
|
||
|
||
Better-Auth-Plugin-Hooks:
|
||
- **Signup-Hook**: legt `personal`-Space mit Slug `@{user.username || user.id}` an, setzt als `activeOrganizationId`.
|
||
- **Organization.create-Hook**: verlangt `type` in `metadata`, validiert gegen enum.
|
||
- **Organization.delete-Hook**: verweigert Löschung, wenn `type='personal'`.
|
||
|
||
**JWT bleibt minimal.** Laut Architektur-Entscheid 2024-12 (siehe `services/mana-auth/src/auth/better-auth.config.ts:59-74`) stehen Org-Claims bewusst NICHT im JWT-Payload. Der aktive Space wird über die Session (Cookie) + `GET /organization/get-active-member` aufgelöst. Der Scope-Wrapper (siehe unten) liest daher aus einem **client-seitigen `$lib/stores/active-space.svelte.ts`**, der beim Boot einmal den Endpoint anfragt und bei jedem `set-active`-Call refresht. Keine JWT-Erweiterung nötig.
|
||
|
||
### Postgres-Layout
|
||
|
||
Kein eigenes `spaces`-Schema — wir nutzen Better-Auth's `organization`-Tabelle direkt. Für unsere Ergänzungen:
|
||
|
||
```sql
|
||
-- im mana_platform-DB, eigenes Schema
|
||
CREATE SCHEMA spaces;
|
||
|
||
-- Space-Credentials (OAuth-Tokens pro Space, z.B. LinkedIn für Edisconet)
|
||
CREATE TABLE spaces.credentials (
|
||
space_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
||
provider TEXT NOT NULL, -- 'linkedin', 'stripe', 'twilio', ...
|
||
access_token_encrypted BYTEA NOT NULL,
|
||
refresh_token_encrypted BYTEA,
|
||
expires_at TIMESTAMPTZ,
|
||
scopes TEXT[],
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
PRIMARY KEY (space_id, provider)
|
||
);
|
||
|
||
-- Per-Modul-Permissions im Space (feingranular über Better-Auth-Rollen hinaus)
|
||
CREATE TABLE spaces.module_permissions (
|
||
space_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
||
role TEXT NOT NULL, -- 'owner' | 'admin' | 'member' | custom
|
||
module_id TEXT NOT NULL, -- 'club-finance', 'social-relay', ...
|
||
can_read BOOLEAN NOT NULL DEFAULT TRUE,
|
||
can_write BOOLEAN NOT NULL DEFAULT FALSE,
|
||
can_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||
PRIMARY KEY (space_id, role, module_id)
|
||
);
|
||
```
|
||
|
||
### mana_sync-DB
|
||
|
||
Wird um `space_id`-Spalte erweitert. RLS-Policy:
|
||
|
||
```sql
|
||
USING (
|
||
space_id IN (
|
||
SELECT organization_id FROM member WHERE user_id = current_user_id()
|
||
)
|
||
)
|
||
```
|
||
|
||
Der bestehende `appId`-Mechanismus bleibt — die Partition wird von `(user, appId)` zu `(space, appId)`.
|
||
|
||
---
|
||
|
||
## Scope-Wrapper (Frontend)
|
||
|
||
```
|
||
apps/mana/apps/web/src/lib/data/scope/
|
||
├── active-space.svelte.ts # reactive: { id, slug, type, role, permissions }
|
||
├── scoped-db.ts # Proxy um db, alle Queries bekommen spaceId-Filter
|
||
├── visibility.ts # applyVisibility(records, currentUserId)
|
||
└── migrations/
|
||
└── v27-add-space-scope.ts # Backfill alle existierenden Records → personal-Space
|
||
```
|
||
|
||
```ts
|
||
// Verwendung in Modulen
|
||
import { scoped } from '$lib/data/scope/scoped-db'
|
||
|
||
// statt: db.contacts.where({ ... }).toArray()
|
||
await scoped.contacts.where({ ... }).toArray()
|
||
|
||
// Escape-Hatch für Cross-Space (Home-Dashboard, globale Suche)
|
||
await scoped.contacts.acrossSpaces().where({ ... }).toArray()
|
||
```
|
||
|
||
Der Proxy wendet auto `where({ spaceId: activeSpace.id })` und filtert `visibility='private' && authorId !== me` heraus.
|
||
|
||
### Modul-Zugriff vor dem Wrapper
|
||
|
||
Zusätzlich zur `spaceId`-Filterung prüft der Wrapper gegen die `SPACE_MODULES`-Allowlist:
|
||
|
||
```ts
|
||
// scoped.club-finance in einem Personal-Space → wirft ModuleNotInSpaceError
|
||
// scoped.meditate in einem Brand-Space → dito
|
||
```
|
||
|
||
Das verhindert, dass ein Modul, das UI-seitig eigentlich ausgeblendet ist, über direkte Store-Calls trotzdem Daten schreibt.
|
||
|
||
---
|
||
|
||
## URL-Topologie
|
||
|
||
Slack/Linear/Notion-Muster:
|
||
|
||
```
|
||
mana.how/@{slug}/{module}/...
|
||
mana.how/@me/mood/today ← Personal-Space
|
||
mana.how/@edisconet/social-relay ← Brand-Space
|
||
mana.how/@turnverein/club-finance ← Club-Space
|
||
```
|
||
|
||
- SvelteKit: `+layout.ts` auf `/(app)/@[slug]/+layout.ts` resolved Space über Better-Auth-Slug-API
|
||
- Middleware: Non-Member → 404 (nicht 403, um Space-Existenz nicht zu leaken)
|
||
- Altes `activeOrganizationId` in Session wird gesetzt, damit Requests gegen `apps/api` den Scope kennen
|
||
- Space-Switcher-UI: klickt auf Space → `goto('/@${newSlug}/home')` statt Session-Manipulation
|
||
|
||
**Migration bestehender Routes**: alle Routes von `/(app)/{module}` auf `/(app)/@[slug]/{module}` verschieben. `@me` als Auto-Redirect für die alten URLs.
|
||
|
||
---
|
||
|
||
## Encryption
|
||
|
||
Drei-Stufen-Modell ersetzt die heutige User-Key-Logik:
|
||
|
||
1. **Personal-Space**: User-Key wie heute (master-key aus `mana-auth`, `MANA_AUTH_KEK`-gewrappt). Zero-Knowledge bleibt möglich.
|
||
2. **Shared-Space** (>1 Member): Space-Key, beim Space-Create erzeugt, KEK-gewrappt an jedes Mitglied über dessen Pubkey. Re-Wrap bei Member-Add. Member-Remove = Key-Rotation-Event (neue Keys für neue Records; alte bleiben lesbar — "eventual re-encryption" als Background-Job).
|
||
3. **Private Records innerhalb eines Shared-Space** (`visibility='private'`): zusätzlich mit User-Key re-encrypted, nur Autor kann lesen.
|
||
|
||
Die Encryption-Registry (`apps/mana/apps/web/src/lib/data/crypto/registry.ts`) muss für jede Tabelle wissen, welche Felder verschlüsselt werden — das bleibt wie heute. Was sich ändert: `encryptRecord()` wählt den Key aufgrund von `(spaceId, visibility)` statt konstant den User-Key.
|
||
|
||
**Phase-1-Kompromiss**: Shared-Space-Daten initial **unverschlüsselt** lassen (nur im Server-RLS-gesichert). Zero-Knowledge für Shared-Spaces kommt als eigenes RFC (`docs/plans/shared-space-encryption.md`), weil Key-Rotation und Pubkey-Infrastruktur für User nicht-trivial sind. Personal-Space behält E2E-Encryption wie heute.
|
||
|
||
---
|
||
|
||
## Tier-Logik verschieben
|
||
|
||
Heute: `tier` hängt am User (JWT-Claim). Das führt zum aktuellen `MANA_APPS tier patch`-Workaround.
|
||
|
||
Neu: **Tier hängt am Space.**
|
||
|
||
```ts
|
||
// space.metadata.tier: 'guest' | 'public' | 'beta' | 'alpha' | 'founder'
|
||
// Modul-Gate: requiredTier wird gegen activeSpace.tier geprüft
|
||
```
|
||
|
||
- Personal-Space eines Founder-Users → `tier='founder'`, alle Module
|
||
- Ein Brand-Space, den derselbe User anlegt → default `tier='beta'`, je nach Plan
|
||
- Admin-API `PUT /api/v1/admin/spaces/:id/tier` ersetzt das alte User-Tier-API
|
||
|
||
Das löst den bestehenden Tier-Patch-Workaround gleich mit auf und passt zum Billing-Modell der ClubDesk-Roadmap (Vereine zahlen pro Space, nicht User).
|
||
|
||
---
|
||
|
||
## Sync-Engine-Änderung
|
||
|
||
`mana-sync` (Go, 3050) bekommt:
|
||
|
||
1. **`space_id`-Column** in allen sync-relevanten Tabellen — NOT NULL, indexed.
|
||
2. **Partition-Key** wird `(space_id, app_id)` statt `(user_id, app_id)`.
|
||
3. **Subscription-Fan-Out**: bei Space-Member-Count > 1 muss ein Change an alle Member gepusht werden (WS/SSE). Heute ist das 1:1 (User pushed to self).
|
||
4. **Membership-Resolver**: der Server muss bei jeder Client-Connection wissen, welche Space-IDs der User sehen darf — aus Better-Auth's `member`-Tabelle.
|
||
|
||
Keine großen Algorithmus-Änderungen am Field-Level-LWW — der Scope partitioniert nur den Keyspace, die Konflikt-Resolution bleibt identisch.
|
||
|
||
---
|
||
|
||
## Migration (alles vor dem ersten Prod-Launch)
|
||
|
||
### Phase 1 — Schema + Better Auth (3–4 Tage)
|
||
1. `metadata.type` zu Better-Auth-Organization hinzufügen, Enum-Validierung im Signup-Hook
|
||
2. `spaces.credentials` + `spaces.module_permissions` Tabellen anlegen
|
||
3. Signup-Hook schreibt Personal-Space mit Slug `@{uniqueUsername}`
|
||
4. Dexie v28 (v27 ist vergeben): `spaceId`, `visibility`, `authorId` auf alle Tabellen
|
||
5. Migration-Script: alle bestehenden Records bekommen `spaceId=personalSpaceId(authorId)`, `visibility='space'`, `authorId=recordUserId`
|
||
6. `SPACE_MODULES` Allowlist in `packages/shared-branding`
|
||
|
||
### Phase 2 — Scope-Wrapper + eines Pilot-Modul (3–4 Tage)
|
||
1. `active-space.svelte.ts`, `scoped-db.ts`, `visibility.ts`
|
||
2. `calendar/` als Pilot umstellen: alle Queries auf `scoped.events...`
|
||
3. Space-Switcher-Komponente in der Shell
|
||
4. `/@[slug]/` Routing-Shell
|
||
|
||
### Phase 3 — URL-Refactor + Module-Rolling-Migration (1–2 Wochen)
|
||
1. Alle `(app)/*`-Routen nach `(app)/@[slug]/*` verschieben
|
||
2. Redirects `/(old-route)` → `/@me/(old-route)` als Übergang (1 Release, dann entfernen)
|
||
3. Modul-für-Modul auf `scoped`-Wrapper umstellen — kann parallelisiert werden
|
||
4. Tier-Logik von User auf Space verschieben; `MANA_APPS tier patch` auflösen
|
||
|
||
### Phase 4 — mana-sync (2–3 Tage)
|
||
1. `space_id`-Column + RLS-Policy
|
||
2. Partition-Key auf `(space_id, app_id)`
|
||
3. Subscription-Fan-Out testen mit 2-Member-Spaces
|
||
4. camt-/TWINT-Integration des ClubDesk-Pakets B baut dann direkt auf Space-Scope auf
|
||
|
||
### Phase 5 — Shared-Space-Encryption (eigenes RFC, späterer Zeitpunkt)
|
||
Siehe Encryption-Abschnitt. Nicht blockierend für Launch, kommt bei Bedarf.
|
||
|
||
---
|
||
|
||
## Konsequenzen / neue Anforderungen
|
||
|
||
### Space-Create-Flow
|
||
UI-Flow "Neuen Space anlegen" muss Typ-Auswahl anbieten mit Kurzbeschreibung. Templates (z.B. "Verein" → lädt `club`-Grundstruktur) wären wünschenswert, aber Phase 2.
|
||
|
||
### Cross-Space-Dashboards
|
||
- **Home** (`/@me/home`): aggregiert "heute anstehend" aus allen Spaces, die der User sieht. Nutzt `scoped.X.acrossSpaces()`.
|
||
- **Globale Suche**: Default = aktiver Space. Toggle "alle Spaces" sichtbar.
|
||
- **Notification-Feed**: immer Space-tagged (`[Edisconet] ...`, `[Turnverein] ...`).
|
||
|
||
### Space-Discovery / Invite-Flow
|
||
Better Auth liefert den Invite-Token-Flow. UI: Share-Link mit Rolle, E-Mail-Invite mit Rolle, Join-Page mit Space-Preview vor Accept.
|
||
|
||
### Profile-Modul wird bi-skopig
|
||
Das bestehende `profile/`-Modul bekommt zwei Modi: "User-Profil" (persönliche Daten, Timezone, Sprache) und "Space-Profil" (Logo, Voice-Doc, legal entity, Tools). Beide leben im aktiven Space — Space vom Typ `personal` zeigt User-Profil-Variante, andere Typen zeigen Space-Profil-Variante.
|
||
|
||
### Module, die fachlich nicht teilbar sind
|
||
Via `SPACE_MODULES` in Nicht-Personal-Spaces gar nicht erst erreichbar. Kein Code im Modul selber nötig.
|
||
|
||
---
|
||
|
||
## Was NICHT in diesem RFC entschieden wird
|
||
|
||
- **Konkrete ClubDesk-Module** (`clubs/`, `club-finance`): folgen einem eigenen Plan, bauen aber auf Spaces auf
|
||
- **Social-Relay-Implementierung**: der bestehende `social-relay-module.md` wird nach diesem Foundation-Plan überarbeitet (Brand wird zum Space, nicht eigenes `brands/`-Modul)
|
||
- **Shared-Space-Encryption**: eigenes RFC (Phase 5)
|
||
- **Billing-Integration** (Stripe pro Space): späterer Plan, braucht Spaces als Voraussetzung
|
||
- **Multi-Space-Parallel-Views** (Slack-Stil mit mehreren Spaces gleichzeitig sichtbar): Phase-2-Idee, nicht Launch-kritisch
|
||
|
||
---
|
||
|
||
## Bekannte Altlast: `spaceId`-Namenskollision
|
||
|
||
Vier bestehende Dexie-Tabellen nutzen das Feld `spaceId` bereits für das
|
||
**ältere** Context-Space-Konzept (chat-/memoro-interne Kontext-Ordner,
|
||
nicht das neue Multi-Tenancy-Space):
|
||
|
||
- `conversations` (chat) — `spaceId` → `contextSpaces.id`
|
||
- `documents` (context) — `spaceId` → `contextSpaces.id`
|
||
- `spaceMembers` (memoro) — `spaceId` → `contextSpaces.id`
|
||
- `memoSpaces` (memoro) — `spaceId` → `contextSpaces.id`
|
||
|
||
Die v28-Migration hat diese Tabellen **nicht korrumpiert**, weil der
|
||
Stamping-Code nur fehlende `spaceId`-Felder setzt (`if undefined/null`).
|
||
Bestehende Records mit Context-Space-Referenzen sind unverändert.
|
||
|
||
**Follow-up**: Rename `spaceId` → `contextSpaceId` auf diesen vier Tabellen
|
||
+ ihren Modulen + Dexie-v29-Migration, damit das Namensfeld eindeutig der
|
||
neuen Space-Primitive gehört. Bis dahin ist der Scope-Wrapper für diese
|
||
Tabellen nicht verwendbar — entweder Kollision erst fixen oder das
|
||
Wrapper-Filter per Modul-Ausnahme deaktivieren. Calendar, Todo, Notes etc.
|
||
sind nicht betroffen.
|
||
|
||
## Offene Fragen
|
||
|
||
- **Slug-Uniqueness-Kollision**: User Till mit `@till` kollidiert mit potentiellem Brand `@till`. Lösungsraum: Slugs global unique (einfach, aber Race um beliebte Namen) vs. Slug-Präfixe (`@user/till` vs. `@org/till` — hässlich). Vorschlag: global unique, First-Come-First-Served, User-Slug bei Signup aus E-Mail-Local-Part + Suffix bei Kollision.
|
||
- **Default-Visibility neuer Felder**: `visibility='space'` standard. Aber z.B. Mood-Einträge in einem Shared-Family-Space wären `visibility='private'` per Modul-Config sinnvoll. Brauchen wir ein `defaultVisibility` pro Modul-Tabelle?
|
||
- **Space-Löschung**: was passiert mit veröffentlichten Inhalten (z.B. LinkedIn-Posts via Social Relay)? Soft-Delete mit Re-Activation-Period? Hard-Delete mit Warnungen? Orga-Entscheidung, nicht Architektur.
|
||
- **Anonymes Space-Sharing** (z.B. Event-Anmelde-Link ohne Account — existiert bereits in `mana-events`): bleibt wie heute, ist public-by-token und orthogonal zum Space-Scope.
|
||
- **Werden User-Tables in Better-Auth selbst ge-space't?** Nein: User, Session, Account sind identity-global. Nur unsere Domain-Daten werden ge-space't.
|
||
- **Slug-Änderungen**: darf ein Space seinen Slug ändern? Ja, mit 301-Redirect über alte Slugs (braucht `space_slugs_history`-Tabelle). Phase 2.
|
||
- **Welches Modul bekommt `social-relay` zum ersten Einsatz?** Logisch: nachdem Spaces stehen, Social Relay als *erster* neuer Modul-Code nach der Foundation — das stress-testet das ganze Modell mit einer echten Brand-Space-Anwendung.
|
||
|
||
---
|
||
|
||
## Zusammenfassung
|
||
|
||
Space-first macht 120+ Module strukturell gleich, eliminiert die User-vs-Org-Polymorphie, nutzt Better Auth maximal, und bildet die Grundlage für alles, was mit Team-/Brand-/Verein-Features kommt. Der Aufwand ist ein einmaliger Schema-Umbau jetzt (geschätzt 3–4 Wochen bei konzentrierter Arbeit), bevor Prod-Daten existieren. Das Alternative (später nachrüsten) wäre ein mehrmonatiges Migrationsprojekt mit Downtime.
|
||
|
||
**Reihenfolge kritischer Abhängigkeiten:**
|
||
1. Phase 1 (Schema) — blockiert alles weitere
|
||
2. Phase 2 (Wrapper + Pilot) — validiert das Modell an einem echten Modul
|
||
3. Phase 3 + 4 (Rolling-Migration) — parallelisierbar
|
||
4. Social Relay, ClubDesk-Pakete, Billing, Team-Features — alle danach
|