Ägare, inbjudningar & överlämningar (spec 1 av 2)
Created: Wed 15 Jul 2026 10:22:53 CEST
Updated: Wed 15 Jul 2026 18:18:18 CEST — Rättade två detaljer mot det som faktiskt skeppades: avsändaradressen med bindestreck ändrad till noreply@plings.io (verifierad avsändare, API PR #122), och flow-doc-filnamnen bjud-in-medlem.md/byt-agare.md → bjud-in-anvandare.md/byt-agare-overlamning.md.
Updated: Thu 16 Jul 2026 06:50:12 CEST — Korrekta ägarförhållanden i de verkliga exemplen (Paul 2026-07-16): sommarstället Saltsjön ägs av ägaren “Långudden” med sex owner-medlemmar (Paul, Katja, Elisabeth, Nina, Kåre, Leiv) — inte av “Paul & Elisabeth”.
Document Version: 1.0 — utkast efter brainstorm med Paul 2026-07-15
Security Classification: Internal Technical Documentation
Target Audience: Backend, iOS, Web
Author: Paul Wisén (design), Claude (spec)
Systerspec:
2026-07-15-owner-relations-visibility-design.md(synlighet mellan ägare, byggs efter denna).
Sammanfattning
Tre saker byggs: (A) e-postinbjudningar som kopplar en ny eller befintlig användare till en ägare (organisation) med vald roll, (B) medlemsvyn som gör inbjudningarna synliga och hanterbara, och (C) gated överlämning — “ge bort lådan med saker” — som generaliserar dagens changeObjectOwner till en transaktion med acceptans-gate och innehålls-kaskad. Målet är spridningseffekt: varje inbjudan och överlämning är ett ögonblick där en ny person dras in i Plings.
Låsta beslut (blir ADR:er i implementations-PR:en)
- Begreppet “ägare”. UI:t säger ägare överallt där systemet säger
Organization(klarspråksprincipen, som “mall” för ObjectClass). Schemat behållerOrganization. Konstellationer utan juridisk person (släktkonstellationen “Långudden”) är giltiga ägare; distinktionen “kan debiteras” införs först när ekonomin byggs. - Ett objekt har exakt en ägare. Delat ägande modelleras som en gemensam ägare med flera
owner-medlemmar (sommarstället Saltsjön ägs av ägaren “Långudden” — sex owner-medlemmar: Paul, Katja, Elisabeth, Nina, Kåre och Leiv).organization_membersstödjer redan flera owners — ingen schemaändring. - Token är nyckeln (inbjudningar). Den inloggade användare som löser in en giltig engångstoken blir medlem, oavsett kontots e-postadress (Apple/Google-inloggning gör adressmatchning opålitlig — “dölj min e-post”-reläer). Skydd: hashad token, engångsbruk, 14 dagars utgångstid, synlig accepthistorik, ta-bort-medlem.
- Överlämning är gated. Mottagande ägarens owner måste acceptera — utom när initieraren själv har owner/admin-roll i den mottagande ägaren (då fullbordas den direkt; man accepterar inte från sig själv).
- Ingen rekursion nu. En ägare kan inte vara medlem i en annan ägare (Intelliray AB som delägare i Långudden). Noteras som framtida utbyggnad; inget i denna design blockerar den.
- Ekonomin är parkerad, medvetet. Framtida modell (Pauls riktning): ägaren bär objektets lagringskostnad; konstellationer får eget konto/saldo som fördelas pro rata på medlemmarnas andel → medlemskapet behöver så småningom ett andelsfält. Fältet är rent additivt (nullbar kolumn) — inget vi bygger nu målar in oss.
- Framtida NFT-gate. Ägarskap ska kunna representeras av en NFT på kedja (Solana eller Cardano). När det byggs blir kedje-bekräftelsen ytterligare ett gate-villkor i överlämningens livscykel — arkitekturen (transaktion med status + gates) är vald för att det ska vara ett tillägg, inte en ombyggnad.
Nuläge (verifierat i kod/DB 2026-07-07)
organizations(id, name, type Individual/Family/Business/Organization, created_by, description) +organization_members(org, user, role owner/admin/member, PK (org,user)). Endastownersätts i praktiken.- Queries/mutationer:
myOrganizations,createOrganization,updateOrganization,setDefaultOrganization. Ingen invite-/medlemshantering; iOS-stubben “Bjud in användare — Kommer snart”. changeObjectOwnerfinns (enskilt objekt, membership-checkad, dubbelskrivning PG+Neo4j). Kaskad/subträd saknas (låg redan i API-backloggen).- Authz går via
organization_members(inte JWT-claimen) sedan PR #86.
Del A — Inbjudningar
Datamodell (Postgres)
Ny tabell organization_invitations:
| Kolumn | Typ | Anm |
|---|---|---|
id |
uuid PK | |
organization_id |
uuid FK | ägaren |
invited_by |
uuid FK auth.users | |
email |
text | dit mailet gick (informativt — binder inte inlösen, se beslut 3) |
role |
text CHECK owner/member | vald roll; admin exponeras inte ännu |
token_hash |
text unik | SHA-256 av token; klartext-token finns bara i mailet |
language |
text default ‘sv’ | mallspråk; fältet finns från dag ett |
status |
text | pending / accepted / revoked / expired |
expires_at |
timestamptz | nu + 14 dagar |
accepted_by / accepted_at |
uuid / timestamptz | vem som löste in |
created_at |
timestamptz |
GraphQL (alla membership-/rollgrindade på ägaren)
inviteToOrganization(organizationId, email, role, language)→ skapar rad + skickar mail. Endast owner (senare admin). Rate-limit per ägare/dygn.revokeInvitation(id),resendInvitation(id)(ny token, gamla ogiltigas).acceptInvitation(token)→ kräver inloggning; validerar hash + status + utgång; skaparorganization_members-rad med inbjudans roll (idempotent: redan medlem → ok); markerar accepted.organizationMembers(organizationId)→ medlemmar (namn/e-post via profiles, roll, joined_at) + väntande inbjudningar. Synlig för medlemmar; hantering kräver owner.removeOrganizationMember(organizationId, userId)→ owner-grindad; sista owner kan inte tas bort (guard).
E-post (Resend)
- Ny modul i Plings-API (
app/emails.pye.d.): Resend REST viahttpx(återanvänd, ejrequests), API-nyckel i env (RESEND_API_KEY), avsändarenoreply@plings.io(domänverifiering i Resend krävs — driftsteg). - Mallar nycklade på
language(‘sv’ nu): ämne + HTML + text. Mallfiler, inte inline-strängar, så språktillägg = ny fil. - Mailet: vem bjuder in, till vilken ägare, roll, CTA-länk
https://plings.io/invite/<token>, giltighetstid. - Fel i mailsändning får inte tappa inbjudan: raden skapas först, sändningen är retry-bar (
resendInvitation).
Webb-landning plings.io/invite/<token> (Plings-Web)
Mottagaren har ofta inte appen — sidan är spridningsögonblicket och ska vara säljande + tydlig:
- Oinloggad: förklarar Plings kort, visar “Paul har bjudit in dig till ägaren Göran Hamberg”, knapp till inloggning/kontoskapande (Supabase Auth, befintliga flöden) med retur till sidan.
- Inloggad: “Acceptera inbjudan” →
acceptInvitation→ bekräftelse + länk/instruktion till appen (TestFlight under testfasen). - Ogiltig/utgången token → vänligt felläge med kontaktväg till inbjudaren.
iOS
- “Bjud in användare”-stubben blir riktig: formulär (e-post + roll Ägare/Medlem) → mutationen. Optimistiskt: inbjudan syns direkt som “väntar” (lokal-först; sändningen är bakgrundsjobb i kön som allt annat).
- Samma skärm blir medlemsvyn: medlemmar med roll, väntande inbjudningar (återkalla/skicka igen), ta bort medlem. Klarspråkscopy (“Ägare”, “Medlem”, “Väntar på svar”).
- Småfix i samma slice: typ-etiketten visar rå “Business” — översätts (“Företag” m.fl.).
Del B — Överlämning (“ge bort med innehåll”)
Datamodell (Postgres)
Ny tabell ownership_transfers:
| Kolumn | Typ | Anm |
|---|---|---|
id |
uuid PK | |
root_object_id |
uuid | det valda rotobjektet |
object_ids |
uuid[] | fryst lista — exakt det som byter ägare |
from_organization_id / to_organization_id |
uuid | |
initiated_by |
uuid | |
kind |
text | ‘gift’ nu; ‘sale’ senare |
status |
text | pending / accepted / declined / revoked / completed |
created_at / resolved_at |
timestamptz |
Posten arkiveras (raderas inte) — kvittohistoriken som försäljning och NFT-gate senare bygger på.
Flöde
- Initiering (iOS): “Byt ägare” får valet med innehåll. Förhandsgranskning av grenen med urbockning per objekt (plats ≠ ägande — någon annans saker i lådan ska inte följa med). Grenens innehåll: objektets NORMAL-subträd + det som står CURRENT där markerat “står här men hör hemma annanstans — följer inte med om du inte bockar i”. Endast objekt initieraren har ägar-rätt till kan ingå.
- Gate:
initiateOwnershipTransfer(rootObjectId, objectIds, toOrganizationId)→ har initieraren owner/admin i mottagaren ⇒ fullborda direkt (= dagens Göran Hamberg-fall). Annars statuspending: mottagarens owners ser inkommande överlämning i appen + mail via Resend-infran (“Paul vill ge dig Verktygslådan med 12 saker”). - Avgörande:
acceptOwnershipTransfer(id)/declineOwnershipTransfer(id)(mottagar-owner),revokeOwnershipTransfer(id)(initieraren, medan pending). - Vid accept: omvalidera id-listan (borttagna objekt hoppas över och rapporteras i svaret), byt ägare i batch — Postgres
owner_organization_id+ Neo4jownerOrganisationId(brittisk stavning!) i samma mönster somchangeObjectOwner,executemany/UNWIND. Markeracompleted. - Lokal-först: initierarens UI väntar aldrig — överlämningen visas direkt som “väntar på Göran”; statusuppdatering kommer via synk. Mottagarens accept är per definition server-medierad (cross-user) men accept-knappen svarar optimistiskt.
Kända kanter
- Objekt som flyttas/raderas under väntetiden: fryst lista + omvalidering (ovan).
- Objekt med väntande synkjobb hos initieraren: transfern opererar på server-id; temp-id-objekt (ej synkade) kan inte ingå i en gated transfer (UI filtrerar bort dem med förklaring). (Temp→server-id-handoff-buggklassen.)
- Mottagare utan medlemmar-med-app: gaten kräver att mottagarägaren har minst en owner-användare; annars är auto-fullborda-fallet (initieraren har rätt i mottagaren) det enda möjliga — vilket är exakt dagens “jag skapar ägaren åt Göran”-läge.
Ordning & leverans
- Plings-API (migrations, resolvers, Resend, tester) — mergas först.
- Plings-Web (invite-landningen
/invite/<token>) — därefter. - Plings-iOS (medlemsvy + bjud in + överlämnings-UI) — sist.
Docs som skeppas med implementations-PR:erna: publika use-cases i Plings-Web/docs/use-cases/ (Pauls uttryckliga önskan — Göran-överlämningen, Långudden, förskolan; visar användare hur behörigheterna används), flow docs (bjud-in-anvandare.md, byt-agare-overlamning.md i respektive repo), ADR:er för de låsta besluten ovan, ROADMAP-uppdateringar. Den befintliga backlogg-posten “cascade” i Plings-API bockas av av denna spec.
Dessutom (Pauls krav vid spec-godkännandet): den befintliga officiella dokumentationen om organisationer uppdateras i samma leverans så den speglar resonemanget här — ägare-begreppet, delat ägande via gemensam ägare, konstellationer utan juridisk person, medlemsroller och inbjudningar. Kända berörda dokument (inventeras fullt i planen): docs/use-cases/personal-organization.md, docs/core-systems/organization-ownership-intelligence.md, docs/database/SCHEMA-VERIFICATION.md (nya tabeller), samt arkitekturöversikten där organisationer beskrivs. Out-of-date docs är en bugg — detta är en del av definition-of-done, inte ett efterarbete.
Öppna frågor (utflöden)
- Ska accepterad inbjudan även skapa en Vän-relation tillbaka till inbjudarens personliga ägare (spridningsvärde)? → Föreslagen ADR när spec 2 byggs.
- Avsändardomän/DNS för Resend (
noreply@plings.io) — driftsteg som kräver Pauls DNS-åtkomst. - Admin-rollens faktiska rättigheter (finns i DB, exponeras inte) → definieras när behovet uppstår; Backlog.