Ä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.mdbjud-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)

  1. Begreppet “ägare”. UI:t säger ägare överallt där systemet säger Organization (klarspråksprincipen, som “mall” för ObjectClass). Schemat behåller Organization. Konstellationer utan juridisk person (släktkonstellationen “Långudden”) är giltiga ägare; distinktionen “kan debiteras” införs först när ekonomin byggs.
  2. 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_members stödjer redan flera owners — ingen schemaändring.
  3. 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.
  4. Ö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).
  5. 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.
  6. 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.
  7. 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)

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)

E-post (Resend)

Webb-landning plings.io/invite/<token> (Plings-Web)

Mottagaren har ofta inte appen — sidan är spridningsögonblicket och ska vara säljande + tydlig:

  1. 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.
  2. Inloggad: “Acceptera inbjudan” → acceptInvitation → bekräftelse + länk/instruktion till appen (TestFlight under testfasen).
  3. Ogiltig/utgången token → vänligt felläge med kontaktväg till inbjudaren.

iOS

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

  1. 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å.
  2. Gate: initiateOwnershipTransfer(rootObjectId, objectIds, toOrganizationId) → har initieraren owner/admin i mottagaren ⇒ fullborda direkt (= dagens Göran Hamberg-fall). Annars status pending: mottagarens owners ser inkommande överlämning i appen + mail via Resend-infran (“Paul vill ge dig Verktygslådan med 12 saker”).
  3. Avgörande: acceptOwnershipTransfer(id) / declineOwnershipTransfer(id) (mottagar-owner), revokeOwnershipTransfer(id) (initieraren, medan pending).
  4. Vid accept: omvalidera id-listan (borttagna objekt hoppas över och rapporteras i svaret), byt ägare i batch — Postgres owner_organization_id + Neo4j ownerOrganisationId (brittisk stavning!) i samma mönster som changeObjectOwner, executemany/UNWIND. Markera completed.
  5. 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

Ordning & leverans

  1. Plings-API (migrations, resolvers, Resend, tester) — mergas först.
  2. Plings-Web (invite-landningen /invite/<token>) — därefter.
  3. 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)