Seat lifecycle: seats never free — rolling activity window + visible seats + manual release #428
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#428
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Flagged twice this week (Manuel's licensing question; Nikola 2026-09-05): "how would a seat become free again if a person leaves the company and their user either gets deleted from their OIDC or just stays for multiple months?"
Current behaviour: it never does
license.Manager.SeenUsersis append-only.Admitrecords a username on first sign-in; nothing ever removes one, and the stored timestamp is first-seen — not even refreshed on later sign-ins. A leaver holds their seat forever, whether their IdP account is deleted or just dormant. Cairn cannot observe IdP deletion (no SCIM/webhooks; OIDC only speaks at sign-in). The Overview footnote — "seats are people who have signed in, not accounts" — quietly means ever, which makes it a ratchet.How the competition does it
Direct competitors (self-hosted file sync/share):
ACTIVATE_AFTER_FIRST_LOGINoptionally ties activation to first login)Adjacent industry:
Fit assessment for Cairn: the Seafile/Nextcloud/GitLab model requires the product to own account state — a "disable user" concept. Cairn deliberately has none ("Cairn does not store passwords and cannot create, disable or rename a person"). The only models that fit an IdP-consuming product are ownCloud's activity window (no account state needed, purely observed sign-ins) and a manual release on the seat record (the seat, not the account — Cairn owns the former, never the latter).
Design (completes the existing philosophy instead of replacing it)
Admitrefreshes the user's timestamp on every successful sign-in, throttled to once per ~24h per user so it is not a state write per request. Migration is self-healing: existing first-seen values simply act as stale last-seens; active users blink out at worst once and re-record on next sign-in.Admit, and theseatsUsedreported at check-in all use it — displayed, enforced and billed numbers can never disagree (the Nextcloud bug class dies by construction). Dormant leavers age out automatically; IdP-deleted users age out identically since they can never sign in again.Deliberately rejected: account-state seats (nothing to deactivate in Cairn), SCIM/IdP webhooks (an integration surface far out of scope for the problem), GitLab-style max-seats true-up (renewal surprises; check-in stays a point-in-time honest report), ownCloud's 2-week window (too flappy for a billing definition).
Decisions to bless before building
seatsUsedpayload shape is unchanged.)seat-release— confirm.Verification
Related: #426 (admins now occupy seats — this issue governs how any seat is ever released), the licensing dress rehearsal planned for v0.7.
Decisions blessed (Nikola, 2026-09-05):
seatWindowDaysmetadata field provides nothing to the customer and creates a visible "sends usage data" surface in a product sold on sovereignty and privacy. Seafile and ownCloud send nothing back to their vendors; purchased seat counts are already known on our side from the sale. "Technically no one gains anything, but on the human level we might lose trust before even landing." The upgrade-cliff cost (seatsUsed dropping unexplained at the version boundary, incomparable history) is accepted knowingly.seat-release; released users re-occupy on next sign-in.Open question surfaced by the same reasoning (not blocking this issue): the existing check-in already reports
seatsUsed(#89). The sovereignty argument applies to it identically — dropping it would place Cairn fully in the Seafile/ownCloud no-usage-telemetry camp, leaving check-in as pure licence validation (renewals informed by the customer's own Seats tab instead). Nikola to decide separately.Next: Seats tab UI mockup (house rule: mockup before build), then TDD implementation.
Shipped and live-verified. v0.6.129 (PR #429, TDD run
tdd-428— red evidence in the run log: window-aging assertions, api compile-red, Seats/ReleaseSeat compile-red, all watched before their implementations landed in the same run), plus two layout patches v0.6.130/v0.6.131 (the admin shell's#adminview .uarowtwo-column grid wrapped the kebab; fixed with a two-id three-column rule, verified by live style injection before tagging). Both dogfoods run v0.6.131.What went in:
Admitrefreshes the holder's timestamp (throttled to one write per day); windowed 90-day counting feeds the Seats tile, the admission cap and both check-in paths. Legacy first-seen entries count as last-seen — the migration is doing nothing (verified live: manu-admin holds no seat because his last sign-in predates v0.6.128's admin recording; he claims one at next sign-in).GET /api/v1/license/seats+POST /api/v1/license/seats/release; release appends audit actionseat-release(actor = admin, path = freed holder).#admin/users/seats; licence meter footer restates the 90-day definition ×4 locales and links to the tab.Live verification on the enc dogfood (nikola-test session):
04:39 · nikola-test · seat-release · nikola-test2✓Follow-up nit: the audit query page's Action dropdown doesn't list
seat-release(events appear under "Any action"; the dropdown is a hardcoded list).Still open on this issue: the parked question of dropping
seatsUsedfrom the check-in payload entirely (Nikola's call), and Nikola's own look at the live tab before closing.Closing — every thread resolved:
seatsUseddropped from the check-in payload. v0.6.132 (PR #434, TDD with the red watched on the runner) posts{licenseId}alone; seat usage never leaves the instance. The licence server was verified beforehand to require onlylicenseId. Live-verified: Refresh licence on the enc dogfood at 05:32 — check-in OK against license.swisscairn.ch with the slimmed payload, key intact.FreeSeatscomment (PR #436) — and on cairn-site, including the factually false pricing bullet "at or under 50 users never contact us — not even with a key installed" (cairn-site PR #20). Historical CHANGELOG entries (v0.3.2) carry supersession notes rather than rewrites (PR #435).Full arc: v0.6.129 seat lifecycle (90-day window, last-seen refresh, Seats tab, audited release), v0.6.130/131 layout fixes, v0.6.132 payload slim — all live on both dogfoods, docs coherent across handbook, ARCHITECTURE, site, and wikis.