Skip to content

bug(entitlements): subscriber gating covers 3 of ~10 documented features (G37) #40

Closed

Description

@Adron

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:

  1. 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.
  2. Adding rows is free. "Adding rows to an existing list is free, even without a subscription."
  3. Revoking is free. Removing someone's access and revoking a share link work with a lapsed subscription.

What we ship today

  • EntitlementsService.swift:87 — canManageLists returns 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.
  • Feature carries only .mediaAttachments, .scheduledPosts, .crossPosting (EntitlementsService.swift:10–14).
  • DocumentsService has 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 Feature to the documented create-gated set: .listCreation, .listFolderCreation, .documentCreation, .documentTemplateCreation, .organizationCreation, .sharingWithPeople, .emailInvites, .shareLinkCreation, .aiFeatures, alongside the existing three.
  • Replace canManageLists's permissive default with customerStatus-driven logic, and delete the stale PLAN.md comment. Keep the init(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; leave createRow/updateRow ungated (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 — customerStatus missing/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

  1. added
    bugSomething isn't working
    parityWeb-parity gap with the InterlinedList web app
    on Sep 7, 2026
  2. Adron commented on Sep 15, 2026

    @Adron
    MemberAuthor

    Closed by PR #83 (merged to dev as 3461baf), verified present on dev.

    Packages/InterlinedDomain/Sources/InterlinedDomain/Services/CapabilityGate.swift ships the Feature matrix at 11 cases (up from 3), and the nine TODO(#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. GatingMatrixTests asserts both directions, including that a gated action is refused before the HTTP call.

    There is deliberately no listFolderCreation case — see the owner decision on #49.

    GitHub did not auto-close it because PR #83 merged into dev, not the default branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions