Bjud in användare — flow doc
Living doc — how inviting a user to an owner works now. For why, see ADR-0077, ADR-0079 and the use-case (UC-103). For history, see the spec. This doc is rewritten in place as the flow changes; it does not restate the ADR/spec.
What it is
A way to bring a person into an ägare (owner — the public word for an Organization)
with a chosen role. The invitee gets an email, opens a public landing page at
plings.io/invite/<token>, logs in or creates an account, and becomes a member. Every
invitation is a spridningsögonblick — the moment a new person enters Plings.
How it works
1. Invite (GraphQL, owner-gated, rate-limited)
inviteToOrganization(organizationId, email, role, language)creates a row inorganization_invitationsand sends the email. Owner-gated on the target owner (membership check viaorganization_members, not the JWT claim — since PR #86).- Rate limit: max 20 inbjudningar per ägare och dygn.
- The row is created first, then the email is sent, so a mail failure never loses the
invitation —
resendInvitationissues a new token (invalidating the old one) and retries.revokeInvitationcancels a pending one. roleisownerormember(adminexists in data but is not exposed in UI yet).
2. Token is the key
- Each invitation carries a one-time token. Only its SHA-256 hash is stored
(
token_hash); the clear-text token exists only in the email link. - The token is single-use and valid for 14 days (
expires_at). - Whoever is logged in when they redeem a valid token becomes the member — the account’s email address does not have to match the invited address. This is deliberate: Apple/ Google “hide my email” relays make address matching unreliable (ADR-0079). Protections instead of address matching: hashed token, single use, 14-day expiry, visible accept history, and remove-member.
3. Email (Resend, Swedish, i18n-ready)
- Sent via Resend (REST over
httpx) fromnoreply@plings.io(domain verified in Resend — an ops step). - Templates are keyed on
language(Swedish today) and live as template files, not inline strings, so adding a language is a new file. Thelanguagecolumn exists from day one. - The mail states who is inviting, to which ägare, the role, the validity window, and a CTA
to
https://plings.io/invite/<token>.
4. Landing page plings.io/invite/<token> (Plings-Web — live)
The recipient often does not have the app, so this page is the selling + clarity moment:
- Public preview: the page fetches
invitationPreview(token)and shows, without login, “Paul har bjudit in dig till ägaren Göran Hamberg” plus a short pitch for Plings. - Login / signup: handled by the existing Supabase Auth modal, returning to the page.
- Accept: requires being logged in →
acceptInvitation(token)validates hash + status + expiry and creates theorganization_membersrow with the invitation’s role. It is idempotent — already a member → still OK. On success: confirmation + a link/instruction to the app (TestFlight during the test phase). - Invalid / expired token: a friendly error state with a way to contact the inviter.
5. Membership view
organizationMembers(organizationId)returns members (name/email via profiles, role, joined_at) plus pending invitations. Visible to members; managing them requires the Ägare role.removeOrganizationMember(organizationId, userId)is owner-gated, with a guard so the last owner cannot be removed.
Current UI status
- Web: the
/invite/<token>landing page is live (public preview → auth → accept). - iOS: the “Bjud in användare” stub and the member view (list, pending invites, remove-member, role labels) are in progress. The owner type label (“Business” → “Företag”) is being localised in the same slice. Update this section when iOS lands.
Boundaries
- No ägare-in-ägare (recursion) — a person is invited, not another owner.
- Membership grants access to everything the owner owns; there is no per-object sharing here.
- Sender domain/DNS for
noreply@plings.iois an ops prerequisite, not app logic.
Related
- Use-case: UC-103
- Decisions: ADR-0077 (ägare = publikt ord), ADR-0079 (token är nyckeln) — see
internal/decisions/INDEX.md - Sibling flow: byt-agare-overlamning.md
- History:
docs/specs/2026-07-15-owner-invitations-and-transfers-design.md