← The specification

Household identity & invitations

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted · Audience: Both · Area: D — Content & household


Purpose

Get everyone else in the home an account, without the operator learning Jellyfin’s user administration.

Adding a household member currently means: open Jellyfin, find user management, create a user, set a password, decide library access and parental limits, then tell the person their credentials over a messaging app — which is both awkward and a poor way to handle a password.

The operator does this a handful of times, always from a cold start, always having forgotten where the settings are.

Behaviour

One account per person, covering everything

A household member’s Jellyfin account is their identity for both watching (Jellyfin) and requesting (Seerr, authenticating against it). One credential, created once.

Invitations rather than credential handover

The operator creates an invitation; the household member sets their own password.

$ lemonfiber invite ana
  ✓ ana can sign in — unclaimed until she sets a password, and expires in 48 hours

  Send this:
  http://192.168.1.20:8096

  [QR code]

  Tell her to sign in as `ana`. She will be asked to set a password.
  If she would rather not, she can decline it here:
  http://192.168.1.20:8097/decline/7f3c9a

Both addresses are answered by the stack, never by a process that lives only as long as an operator’s session. lemonfiber ui keeps nothing running once the operator closes it (G1-R5), so an invitation that only worked while it did would stop working the moment the operator went to bed. What matters is how long the process lives, not who built it: the decline page is served by a lemonfiber-built image that the stack runs under its restart policy (ADR-0029). Accepting is a state Jellyfin holds: the account is there, unclaimed, until somebody sets its password. Declining is a page, and the stack serves it from a service it keeps running under its restart policy, beside the household front door and at the same binding tier (C6), which is where the invitee already is. The front door itself is Seerr or Jellyfin, and lemonfiber adds no route to either.

A refusal is the invitation’s standing. The page declines the one invitation its address names and nothing else. The service answering it disables the account at once, so it can no longer be signed in to or claimed, and keeps the refusal until the core next reads the household, which reports the invitation as declined rather than expired. A declined account is kept, disabled, until the operator removes it or re-issues the invitation: it is not swept when 48 hours pass, because a refusal is something the operator should see before it goes.

The sign-in address is Jellyfin’s, which is not always the front door. Setting a first password happens in Jellyfin, and Seerr authenticates against Jellyfin rather than holding credentials of its own — so an account with no password yet cannot be claimed through Seerr, whichever service G5 picks as the door. What G5-R14 is for still holds, and is what the operator experiences: one address, chosen by lemonfiber, rather than four to choose between. After the account is claimed, the household member uses the front door like everyone else.

What invite does is create the account (D6-R12 says so plainly — an invitation issued while Seerr is down still creates it), leave it without a password, and record that it is unclaimed. The address is the same one the household is given for everything else.

That is also what makes it single-use and expiring in the way D6-R3 asks (D6-R4 covers the QR). It is claimed once, because setting a password is something that happens once and cannot be undone by a second arrival; and it expires because an account nobody claimed within 48 hours is removed rather than left standing as a way in that nobody is watching.

The stack takes an invitation back when its window closes, whether or not a lemonfiber process is running, because a way in that waits for the operator’s next command stands open for as long as the operator is away. The decline service checks once a minute. An account nobody was ever seen in is removed; one somebody has been in, which is a reset nobody took up, is switched off and kept, because it holds what they watched. It removes an account only where a read made just before the removal shows the account is the one the invitation names, under the same issue time, with no password, no administrator’s rights and nobody ever seen in it; anything else, or a read that does not answer, switches it off or leaves it as it is. The core reports an invitation taken back either way as expired, and never as an account that went missing (ADR-0029 §2a).

An invitation also opens the app. Beside the address somebody signs in at, the core mints a join link: the companion’s own address for this invitation, in the app’s own scheme, lemonfiber://join. It carries what the app needs to reach the house and know it — the address pairing material names, the fingerprint of the certificate that address presents, and the stack’s identifier — with the name the person signs in as, when the link stops being good, and a claim token. A phone that opens it adds the house, asks the person to choose a password, and lands them on Home (N3, N3-R26).

The claim token is the invitation in the app’s hands. It lets the person set their first password through the core, once, within the invitation’s window, acting as their own account at the media server and never as the administrator. The core keeps it only as a hash on the offer, as it keeps the decline token, so a copy of the record claims nothing, and every new offer or reissue mints a new one. Somebody already in the house is given a link with no claim token: it adds the house to their phone, and they sign in with the password they have.

The link is the app’s own scheme rather than a web address on the house, because a house on the home network has no public name and no publicly trusted certificate, and a phone opens a web address in an app only for names fixed when the app was built and checked over the internet. A scheme opens on any network, with nothing reached first. On a phone without the app it opens nothing, which is why the invitation still carries the address somebody signs in at in a browser.

Where the web surface has not been served encrypted on the network, the core has no address a phone could pin, so the invitation is made all the same, without a join link, and says why. It never composes an address to fill the gap.

The operator never chooses or transmits someone else’s password.

The QR code matters more than it looks: the recipient is usually holding the phone they’ll watch on, and typing a LAN URL and credentials on a phone keyboard is exactly the friction that makes people give up.

Access is decided at invitation, in plain terms

Which libraries, and any age limit (D8) — asked as “what should Ana be able to see?”, not as a permissions matrix.

Household members are listed and manageable in one place

Who exists, what they can access, when they last watched, and what they’ve requested — without opening two web UIs.

Removal is complete and honest

Removing someone revokes access to both Jellyfin and Seerr, and states what happens to their watch history and outstanding requests. Partial removal — no longer able to watch but requests still arriving — is a confusing state to leave behind.

What the removal says about watch history is part of its machine-readable answer, beside the requests it counts, so a surface that is not the terminal states it in the stack’s words rather than its own (D6-R18). And the yes to a removal is the offer its reading answered, not a bare confirmation: reading what removing somebody costs and agreeing to it are two requests, and between them a request can be made or an account can change. The removal builds the offer again from what the services hold when it runs and refuses by name where anything moved, so a yes agrees to the cost that was read (D6-R17).

LAN-only, and said plainly

Household access works on the home network. Watching from elsewhere is not supported in 1.0 (B7 deferred), and the invitation states this rather than letting someone discover it at a friend’s house.

On the companion

Access is decided at invitation, in plain terms becomes several requirements, because an invitation sent from a phone is the one most likely to be sent without its terms being read (N9). N9-R5 requires an invitation to state what it grants — libraries, filtering, unrated material, and whether requests may be made — before it is sent, and N9-R6 requires it to state when it lapses.

Afterwards, N9-R7 requires two outcomes kept apart: lapsed unaccepted, and declined by the invitee (D6-R16). From the operator’s end they look identical — nobody arrived — and each has a different next move, which is the argument D9 makes about content that never appeared, applied to people.

LAN-only, and said plainly becomes N9-R9: where something works only on the household network, the app says so. That matters more here than anywhere else, because the app is the thing most likely to be held at a friend’s house.

Removal is complete and honest is where rounding does the most damage. N13-R1 requires how far a revocation reached shown as everywhere, media-server-only or nothing, and N13-R2 forbids rendering the middle one as complete (N13). That middle state is precisely the one this feature refuses to leave behind — and flattening it into removed is how it gets left behind anyway. N13-R19 carries D6-R9 to the phone: what removal does to watch history and to requests is stated before it is agreed to, with every finding in the stack’s words.

A member’s name, and a member who is nobody in particular

A member’s session is answered with the member’s display name as the media server holds it, so a surface can greet them without asking. The operator can also be answered with what a member invited with the household’s defaults would be told and could watch — the household’s default access and allowance, read for nobody — so the member’s side can be seen without reading any member’s requests or watch history.

States

Per household member:

State Meaning
invited Invitation issued, not yet accepted
expired Invitation lapsed unused
declined The invitee refused the invitation at its decline address
active Account created and usable
restricted Active with content or library limits
suspended Access temporarily withdrawn; account retained
removed Access revoked everywhere

Edge cases

Situation Behaviour
Invitation link intercepted on the LAN Single-use and short-lived. State that it grants account creation to whoever opens it. Whoever holds the decline address can refuse the invitation, which costs a re-issue and grants nothing.
Invitation expires unused The stack takes it back within a minute: an account nobody was seen in is removed, and a reset is switched off. The core reports it as expired. Re-issuable without recreating the account definition.
Invitee declines The account is disabled at once and can no longer be claimed. The core reports the invitation as declined rather than expired, and the account stays, disabled, until the operator removes it or re-issues the invitation.
Decline address opened after the invitation was claimed or lapsed Says the invitation is no longer open, and changes nothing.
The stack is stopped when the invitee opens the decline address Nothing answers, as with the sign-in address. The invitation stays invited until it lapses.
Invitee sets a weak password Enforce a minimum; keep the message brief and non-lecturing.
Person already has a Jellyfin account Detect and offer to grant Seerr access rather than creating a duplicate.
Seerr unavailable at invitation time Create the Jellyfin account and complete the Seerr link when it returns; report the partial state.
Household member forgets their password The operator can issue a reset link. They never see or set the password themselves.
Removal with outstanding requests State what happens to in-flight requests before confirming.
Removal with watch history Ask whether to retain or delete it; deleting is irreversible.
Operator invites someone while the stack is stopped Requires Jellyfin running; say so rather than failing obscurely.
Two people share a device Supported — Jellyfin handles multiple profiles on one client.
Invitee on a device that can’t scan QR The URL is always shown alongside.
Join link forwarded to, or intercepted by, somebody else Whoever opens it first and can reach the house may claim that one account within its window, as with the sign-in address. It carries no credential to any other account, nothing of the operator, and nothing usable off the household network. The operator sees the account claimed in the household; declining or removing it is the remedy.
Join link opened after the account was claimed The claim is refused as no longer open. The app still adds the house until the link lapses, and the person signs in with their password.
Join link opened after it lapsed The app refuses it before reaching the house, and the core refuses a claim on a lapsed offer whatever the link says: the core’s record decides, not the link.
Join link text replaced on the way A channel that can rewrite the invitation can point somebody at another house to set a password there. That is the trust the sign-in address already rests on; it is stated, not mitigated.
Web surface not served encrypted on the network The invitation is made without a join link and says why, in words every surface can show.
Household member should also be an operator Out of scope. lemonfiber has a single operator; a second person needs host access.

Acceptance criteria

ID Requirement
D6-R1 A household member MUST require only one account, serving both watching and requesting.
D6-R2 The operator MUST NOT set or transmit another person’s password.
D6-R3 Invitations MUST be single-use and MUST expire.
D6-R4 Invitations MUST be presented as both a URL and a QR code.
D6-R5 Library access and any age limit MUST be selectable at invitation time in plain language.
D6-R6 An existing Jellyfin account MUST be detected and reused rather than duplicated.
D6-R7 Household members MUST be listable with their access, activity and requests in one place.
D6-R8 Removal MUST revoke access in both Jellyfin and Seerr.
D6-R9 Removal MUST state the effect on watch history and outstanding requests before confirming.
D6-R10 The operator MUST be able to issue a password reset without learning the password.
D6-R11 Invitations MUST state that access is limited to the home network.
D6-R12 An invitation issued while Seerr is unavailable MUST still create the Jellyfin account and complete the link later, reporting the partial state.
D6-R13 Expired invitations MUST be re-issuable without redefining the member.
D6-R14 lemonfiber MUST NOT grant household members any access to lemonfiber itself.
D6-R15 An invitation MUST carry a decline address whose page offers the invitee a refusal of that invitation only, served by a service the stack keeps running under its restart policy beside the household front door, at the household binding tier, and never by a process whose lifetime is an operator’s session — the CLI, the TUI or lemonfiber ui (G1-R5); who built the serving image does not matter.
D6-R16 A refusal MUST make the invitation unclaimable at once, MUST be kept by the stack until the core reads it, and the core MUST report it as the invitation’s standing, declined, told apart from one that lapsed (expired).
D6-R17 Reading what removing a household member costs MUST answer with an offer naming what it read; the removal MUST take that offer as its yes, alike at the command line and over the web API (ARCH-R48), MUST build it again from what the media server and the request service hold when it runs, and MUST be refused, naming what moved, where the two differ. A bare confirm MUST NOT be a yes to a removal over the web API (N13-R7).
D6-R18 The removal envelope MUST state what removing the person does to their watch history, in the stack’s words, in the reading before the removal and in the answer after it runs, so that no surface writes that statement itself (D6-R9, N13-R19).
D6-R19 The core MUST answer a member’s session with the member’s display name as the media server holds it.
D6-R20 The core MUST answer the operator’s session with what a member invited with the household’s default access and allowance would be told and could watch, and MUST NOT read or name any member’s requests, allowance or watch history to answer it.
D6-R21 When an invitation’s window closes, the stack MUST take it back without a lemonfiber process running: an account nobody was ever seen in MUST be removed, and one somebody has been in MUST be switched off and kept. An account MUST be removed only where reads made just before the removal show that the core’s invitation table names it under the same issue time, that it is not an administrator, that it has no password, and that nobody has ever signed in to it or used it; on any other answer, or none, it MUST be switched off or left as it is. A declined invitation MUST NOT be taken back. The core MUST report an invitation taken back this way as expired, and MUST NOT report a removed one as an account that went missing.
D6-R22 Every invitation that is not a rehearsal MUST carry a join link where the web surface has last been served encrypted on the network and its certificate and address can be read. The link MUST be lemonfiber://join? followed by these parameters, percent-encoded and no others: address, fingerprint, stack, expires and name, and claim exactly where the standing is made, waiting or reset. address, fingerprint and stack MUST be what pairing material carries at that moment (N1-R18, N1-R48, N1-R62), and the core MUST NOT build the address itself; expires MUST be the moment the offer lapses, or for joined the moment the invitation’s hours from now run out, written as every instant is, in whole seconds since the Unix epoch (ARCH-R274); name MUST be the account’s name as the media server holds it; claim MUST be a token of at least 128 random bits minted for this offer.
D6-R23 The core MUST keep a claim token only as a hash on the offer it was minted for. Offering the same person again, or reissuing, MUST mint a new token, and an older token MUST stop claiming anything.
D6-R24 Where no join link can be made (D6-R22), the invitation MUST still be made, MUST carry no join link, and MUST say why in words every surface can show, naming no command (N1-R76). A rehearsal MUST carry no join link and no such sentence, because it mints no token.
D6-R25 A claim MUST be refused unless the token’s hash is on the offer standing for that account, the offer has not lapsed and has not been declined, and the account is unclaimed, not switched off and not an administrator. A claim that succeeds MUST set the password the person chose on their own account at the media server, acting as that account and never as the administrator; MUST spend the token; and MUST open a member session. The operator MUST NOT be able to read or set the chosen password (D6-R2).
D6-R26 A chosen password shorter than the minimum the core enforces for its own credential MUST be refused, naming that minimum, briefly and without lecturing.