Closes the gap between "invite flow UI exists" and "two users in the
same space actually see each other's data". Three pieces land together
because they're meaningless without each other.
mana-auth — new internal endpoint:
GET /api/v1/internal/users/:userId/memberships
Returns [{organizationId, role}, ...] for the user. mana-sync uses
this to populate the multi-member RLS session config.
mana-sync — membership lookup:
new internal/memberships package with an HTTP client + 5 min
per-user cache, fail-open (empty list = pre-Spaces behavior).
Config gets MANA_AUTH_URL (default http://localhost:3001).
Handler.NewHandler takes the Lookup. Every Push/Pull/Stream call
now passes spaceIDsFor(userID) to Store methods.
GetChangesSince + GetAllChangesSince extend their WHERE clause:
WHERE (user_id = $1 OR space_id = ANY($memberSpaces))
so co-members see each other's rows, not just the author.
apps/web — encryption skip for shared-space records:
encryptRecord now checks record.spaceId:
- `_personal:<userId>` sentinel OR no active shared space → encrypt
with user master key (E2E as today).
- Active space resolves to non-personal type AND spaceId matches
that space → skip encryption; write lands plaintext.
decryptRecord is unchanged because its per-field isEncrypted() guard
already passes plaintext through.
Phase-1 compromise: shared-space data is protected by server RLS
only, not E2E. Phase 2 adds per-Space shared keys with per-member
wrap — tracked in docs/plans/spaces-foundation.md.
Plus docs/plans/shared-space-smoketest.md: step-by-step Zwei-User-Test
mit erwarteten Ergebnissen und Debugging-Hinweisen bei Problemen.
Build + go test + web check all green.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.5 KiB
Shared-Space Smoketest
Schritt-für-Schritt-Anleitung, um das Shared-Space-Ende zu validieren. Zwei Nutzer, ein gemeinsamer Space, echtes Sync + Einladungsflow.
Vorbereitung
Lokaler Stack läuft (pnpm docker:up && pnpm run mana:dev). Vor dem Test
einmal sicherstellen:
-
DB-Migrationen aktuell:
cd services/mana-auth && bun run db:pushHolt das
spaces-Schema (v004) mitcredentials+module_permissions. mana-sync's eigene Migration (inkl. multi-member RLS) läuft beim Service-Start automatisch. -
Dev-User vorhanden:
pnpm setup:dev-userLegt drei Accounts an (
tills95@gmail.com,tilljkb@gmail.com,rajiehq@gmail.com), alle mit PasswortAa-123456789und Founder-Tier. Durch den Signup-Hook bekommt jeder automatisch seinen Personal-Space. -
SMTP-Stub aktiv (für Invite-Mails): Stalwart SMTP läuft im
docker-compose.macmini.yml-Stack. Lokal kann man den Invite-Link auch direkt aus den Logs von mana-auth ziehen, falls die Mail nicht ankommt.
Szenario: Familie
Zwei getrennte Browser-Profile (oder ein normales + ein Inkognito-Fenster). Ohne Profil-Trennung teilen sich die Sessions die Cookies, und der Test ist wertlos.
User A (Besitzer)
- Einloggen als
tills95@gmail.com - Oben rechts im Space-Switcher "+ Neuer Space" klicken
- Typ Familie, Name "Schmidt Family", Slug automatisch
- Erstellen → Reload in den neuen Space, Switcher zeigt "Schmidt Family" + grünes Family-Badge
- Im Switcher → "Mitglieder verwalten …"
- Einladen-Formular:
tilljkb@gmail.comalsMitglied→ Senden - Erfolgsmeldung "Einladung an … gesendet", Invite erscheint in "Offene Einladungen"
User B (Eingeladener)
- E-Mail öffnen, Einladungslink anklicken (
/accept-invitation?id=…) - Falls nicht eingeloggt: "Einloggen & annehmen" — bringt zu Login, nach erfolgreicher Authentifizierung automatisch zurück auf Accept-Seite
- Space-Vorschau zeigt: "tills95@gmail.com lädt dich in Schmidt Family ein", Typ-Badge "Familie", Rolle "Mitglied"
- Annehmen klicken → Redirect auf
/, Space-Switcher oben rechts zeigt jetzt "Schmidt Family"
Gemeinsamer Test (wichtig!)
- User A legt im Familien-Space ein Event für morgen an (Kalender-Modul)
- User B öffnet den Kalender — das Event erscheint nach ≤1 s
- User B legt ein Rezept an
- User A sieht das Rezept nach ≤1 s
Isolation-Check (wichtig!)
- User A wechselt auf den Personal-Space (Switcher)
- User A trägt einen privaten Mood-Eintrag ein
- User B wechselt auf den Familien-Space und öffnet den Kalender-Modul — User A's Personal-Daten dürfen nicht sichtbar sein. Mood-Modul ist im Familien-Space eh nicht freigeschaltet (SPACE_MODULE_ALLOWLIST).
Rücktritt-Test
- User A öffnet "Mitglieder verwalten …" und entfernt User B
- User B lädt die Seite neu — Familien-Space ist verschwunden, aktiver Space fällt zurück auf Personal
Was dabei geprüft wird
| Feature | Erwartung |
|---|---|
| Invite-Mail versandt | Stalwart-Log zeigt Mail, oder E-Mail-Inbox hat Link |
Better-Auth accept-invitation |
auth.invitations.status → accepted |
| Member-Datensatz | auth.members hat neuen Row für User B × org |
activeOrganizationId gesetzt |
Session-Cookie zeigt den neuen Space |
| Scope-Filter greift | User B sieht keine Personal-Daten von User A |
| Multi-Member-RLS greift | User B sieht neue Events/Rezepte von User A |
| Membership-Lookup in mana-sync | Keine 404s im mana-sync-Log beim Zugriff |
| Encryption-Skip | Records im Familien-Space werden plaintext synced (prüfen via DevTools → IndexedDB) |
| Rollen-Enforcement | User B kann nicht weitere einladen (Rolle=Mitglied, canManage=false) |
Bekannte Phase-1-Einschränkungen
- Daten im Shared-Space sind unverschlüsselt (nur RLS-geschützt) — im
RFC
docs/plans/spaces-foundation.mdals bewusster Phase-1-Kompromiss dokumentiert. Phase 2 addiert einen pro-Space-Schlüssel mit Per-Member-Wrap. - Subscription-Fan-Out läuft noch per-User — User A's Write triggert Mana-Sync's Pull erst nach seinem eigenen Tick; User B sieht den Eintrag nach bis zu 1 s, nicht instant. Sobald Fan-Out steht, ist das Sub-200ms.
- Role-Änderungen brauchen Cache-Invalidierung: nach Role-Update dauert die mana-sync-Membership-Cache bis zu 5 min. Für Tests nach dem Leave einfach den Service neu starten.
Was tun wenn's nicht klappt
- E-Mail kommt nicht an: in mana-auth-Logs nach
accept-invitation?id=suchen — Better Auth loggt den Link beisendInvitationEmail. - User B sieht Space nicht nach Accept: Browser-DevTools → Cookies →
mana.session_dataauf die neue Session-ID prüfen, dannloadActiveSpacein der Konsole forcen:await (await import('/src/lib/data/scope/index.ts')).loadActiveSpace({ force: true }) - Rezepte erscheinen bei A nicht: mana-sync-Logs auf
[memberships]oder Policy-Denials checken.app.current_user_space_idssollte die Schmidt-Family-Org-ID enthalten. - Kein Access zu Mitglieder-Seite: Personal-Space kann per Design keine Mitglieder haben — vorher auf den Familien-Space wechseln.
Nächste Schritte nach Smoketest
Wenn alle 9+ Checks grün sind: Foundation ist validated. Follow-ups (nicht blockierend):
- Echtes WebSocket-Fan-Out an Space-Mitglieder
- Shared-Space-Encryption mit per-Member-Key-Wrap
- Member-Activity-Log (wer hat wann was geändert)
- Rollen-spezifische Permissions (trainer darf kein club-finance)