Repository navigation
bug(entitlements): subscriber gating covers 3 of ~10 documented features (G37) #40
Description
Summary
The published Free/Subscriber matrix gates far more than the app knows about. EntitlementsService models three features; the platform gates eight or so. A Free account in the macOS app can walk all the way into a create flow and be rejected by the server with an opaque error, where the web would have shown an Upgrade prompt up front.
The documented matrix (/help/settings, /help/account, re-read 2026-09-07)
Free — post messages, follow, browse and read public lists + documents, join organizations, use tags, read templates, export data, and accept any share or email invite (accepting is always free, and the granted access survives a lapsed subscription).
Subscriber — everything in Free, plus:
- creating and managing lists and list folders
- creating documents and document templates
- creating organizations
- sharing lists/documents with named people, and inviting by email
- share links (creating; revoking is always free)
- attaching images and video
- cross-posting (Mastodon / Bluesky / LinkedIn / X)
- scheduling messages
- AI
Two rules that are easy to get wrong and matter here:
- Lapse is create-only. "If your subscription lapses, your existing lists, documents, and organizations stay fully usable: you can still read and edit them. Only creating new … is subscriber-only." So the gate is on create, never on read or edit.
- Adding rows is free. "Adding rows to an existing list is free, even without a subscription."
- Revoking is free. Removing someone's access and revoking a share link work with a lapsed subscription.
What we ship today
EntitlementsService.swift:87—canManageListsreturns the M3 permissive default (listManagementOverride ?? true) with a doc comment saying "PLAN.md §6 M6 ships the real subscriber-driven gating logic". M6 shipped long ago.Featurecarries only.mediaAttachments,.scheduledPosts,.crossPosting(EntitlementsService.swift:10–14).DocumentsServicehas no entitlement gate at all.- Organization create, share-link create, and email-invite create are ungated client-side (PR feat(sharing): App — collaborators, email invites & make-public UI #13 notes say create paths are subscriber-gated — verify each actually is).
Division of labor
Domain — EntitlementsService
- Extend
Featureto the documented create-gated set:.listCreation,.listFolderCreation,.documentCreation,.documentTemplateCreation,.organizationCreation,.sharingWithPeople,.emailInvites,.shareLinkCreation,.aiFeatures, alongside the existing three. - Replace
canManageLists's permissive default withcustomerStatus-driven logic, and delete the stale PLAN.md comment. Keep theinit(customerStatus:canManageLists:)override for tests. - Encode the create-only rule explicitly: name the cases so nobody can accidentally gate an edit or a read on them (
.listCreation, not.lists). Add a doc comment stating that edit/read/row-add/revoke are never gated.
Domain — services
-
DocumentsService— gate create (and create-from-template) on.documentCreation; leave update/read/delete ungated. -
ListsService— gate list create on.listCreation; leavecreateRow/updateRowungated (rows are free). -
OrganizationsService— gate create on.organizationCreation; joining stays free. -
SharingService— gate collaborator-add, role change, email invite, and share-link create; leave remove/revoke ungated.
App
- Every gated entry point disables (or badges) its control with an Upgrade affordance before the user does the work, matching the web's "they appear locked with an Upgrade prompt" pattern. Do not silently hide them — the web shows the control locked so the user learns the feature exists.
- "Manage subscription" links out to the browser (see the subscription-surface issue) rather than offering any native purchase path.
Tests (per feature)
- happy — subscriber → allowed
- invalid — free → denied, with the Upgrade affordance
- upstream-failure —
customerStatusmissing/unknown → fail open for read/edit, closed for create; assert an unknown status never bricks an existing list - boundary — lapsed subscriber can still edit an existing list, add a row, remove a collaborator, and revoke a share link
Acceptance criteria
- A Free account never reaches a server 403 that the client could have predicted.
- A lapsed subscriber can still read, edit, add rows, remove access, and revoke links.
grep -rn "PLAN.md §6 M6" Packages/returns nothing.- Full E2E gate green.
Notes
work-consolidation.md tracks this as G37. Size S–M. Pairs with the email-verification gate and the account-status gate — the three together are "know why the server will say no before asking".
Activity
Adron commentedon Sep 15, 2026
Closed by PR #83 (merged to
devas3461baf), verified present ondev.Packages/InterlinedDomain/Sources/InterlinedDomain/Services/CapabilityGate.swiftships theFeaturematrix at 11 cases (up from 3), and the nineTODO(#40)/TODO(#41)markers that pointed at it from already-merged code are all retired.The settled rule: creation is gated; managing, moving and leaving what you already have is free — a lapsed subscriber keeps existing content fully usable.
GatingMatrixTestsasserts both directions, including that a gated action is refused before the HTTP call.There is deliberately no
listFolderCreationcase — see the owner decision on #49.GitHub did not auto-close it because PR #83 merged into
dev, not the default branch.