Subscriptions — plans instead of per-app purchases

The store sells plans. A plan governs which packages a subscriber can open, and how much model spend it includes on the platform's own key (metered — see below; BYOK stays available on every plan, at provider prices, never resold). The space/storage quotas ride on the plan nodes as data for the platform to enforce.

The ladder

Five plans, as Store/Tier nodes at Admin/Tiers/{id} — seeded create-if-absent (StorePlans.AddTierSeeding), so an operator's re-pricing survives every redeploy:

id rank price model credit / month what it is
free 0 CHF 0 CHF 5 personal integrations, playground, previews
personal 10 29/mo · 290/yr CHF 10 every general course + the everyday toolkit
pro 20 99/mo · 990/yr CHF 40 professional + builder toolkit, social suite, specialist courses
dedicated 25 6 000/mo · 60 000/yr unmetered your own single-tenant instance in our cluster — unlimited, all-access
enterprise 30 contact us unmetered SELF-HOSTED: module licence + deployment + service days

(learn is a legacy alias for personal — same rank, same label. The credit column is includedModelCredit on the tier node; "unmetered" is the separate modelCreditUnmetered flag, never an absent number — see Model credit is METERED below.)

PlanTiers (Store/Licensing) owns the rank rule; a package names its plan in its own index.json → content.tier (repo-authored, like price). Coverage = subscription rank ≥ package rank — except dedicated, which is IsAllAccess and covers everything ("no limit on packages"), while still sitting between pro and enterprise in the lineup.

Which tier a package goes on

Every package root names exactly one of free, personal, pro, enterprise — validate-repos.py → check_package_tiers fails the PR on a missing or unknown value in every plugin repo, because the two readers of a blank tier disagree: the Store's coverage rule (SubscriptionFact.Covers) covers it by NOTHING, while the platform registry (PlanTierRanks.CoversInstance) reads it as the free baseline. dedicated is a plan, never a package marker; learn is the retired alias nothing may stamp anew. The rule that decides the tier is the plan's own pitch, so a package and the Plans card never contradict each other:

tier what belongs there today (2026-09-10, all repos)
free the platform itself and what every plan needs (Store, AI, Essentials, Edu, Training, Stripe, Providers, Mcp, Import, Export, Publish, Notifications, the model providers, the view libraries); personal integrations (Apple, Google, iCloud, Home Assistant, maps); playground and showcase (Chess, Role Play, Three Bodies, Northwind, Cornerstone); Hosting.Instance 45 packages
personal the work integrations (Mail, Teams, Collaboration) and every general course (MeshWeaver.Education) 12
pro the professional toolkit (Business Rules, Data Modelling, Analysis, Indexing, Observability, Approvals, Video, Voice, WebSearch), the builder providers (Claude Code, Copilot), CRM, the social suite (SocialMedia, LinkedIn, X, YouTube, Marketing), and the insurance-track courses (Risk Transfer, Swiss Solvency Test) 20
enterprise the verticals sold by conversation — the insurance platform (MeshWeaver.Reinsurance: Reinsurance, Claims, Underwriting, Pricing, IFRS 17, SST, …), the Manufacturing vertical, fleet Hosting — and Reinsurance Practice, the course that is worked inside those applications 18

A contactEmail belongs on an enterprise root (sold by conversation) and nowhere else: a pro root with a sales contact renders "Let's talk" beside a plan that is bought with a card — two funnels on one card (CRM carried exactly that until 2026-09-10).

A package never requires a package filed ABOVE it. DependencyInstall installs AND entitles the viewer to every unpriced dependency of what they acquired, and a plan-sold dependency carries no price and no contact — so until 2026-09-10 a Personal course that requires an Enterprise application (Reinsurance Practice → Reinsurance, Underwriting, Claims, ReinsuranceDemo) wrote the learner an entitlement to the Enterprise suite: the paywall's back door. Two guards now hold it shut, and they are one rule: DependencyInstall.DispositionOf(content, dependentTier) reports a dependency ranked above its dependent as sold separately at install (the same bucket as a priced one), and check_package_tiers refuses the declaration at the PR wherever both roots live in the same repo. A pre-installed root is exempt from the dependency half — its requires orders provisioning, and onboarding never runs the dependency install for it (Essentials lists Observability, Mail, Teams, WebSearch, Indexing and Approvals for that reason).

The subscription is a standing key

Admin/Subscriptions/{viewer} is ONE Store/Subscription node: the unforgeable record (Admin partition — the viewer cannot write it) and the control plane (the Enrollment shape: patch requestedAction, the watcher reacts, terminal writes clear the action; only a global admin may ask — the invoker is the framework-stamped author).

Surfaces

Model credit is METERED (#1100)

The first of the plan caps to gain enforcement. includedModelCredit is no longer display-only:

A currency mismatch between the model's rate and the plan's allowance is never converted by a rate the meter chose for itself (invented) and never resolved by dropping the odd rows (an under-count, i.e. money). It folds only at a rate a HUMAN declared — a ModelCreditExchangeRate node at Admin/ModelCredit/_Rate-{FROM}-{TO} — and where none is declared it still reaches no verdict, with a log line naming the exact node to create. That refusal is reported ONCE per charge row rather than on every round (core #3235). The full reasoning — including why the conversion happens when the period is totalled rather than when the charge is written — is A currency the meter can fold (AI/ModelCreditCurrency).

A coupon GRANTS a plan

CouponContent.Tier names a PlanTiers id, and since 2026-09-09 it is required: CouponRedemption.Validate refuses a coupon that names none, ahead of the window and the budget, on the free redemption and the paid order alike. Redeeming activates the redeemer's Admin/Subscriptions/{viewer} on that tier — through the ordinary control plane (Subscriptions.RequestActivation writes requestedAction: Activate; the Store/Subscription watcher reconciles, stamps and logs), never a second way to become subscribed.

Buying a plan with a card

Personal and Pro are self-serve (PlanTiers.IsSelfServe — an ALLOW-LIST, deliberately not "paid and priced": Dedicated carries a published CHF 6 000 price and still provisions a single-tenant server somebody has to agree to build, and Enterprise is a licence). Everything else on the ladder is still sold by conversation, and a new plan is not purchasable until it is named in that rule.

The surface: Subscribe goes STRAIGHT to the payment page

Clicking Subscribe on a Plans card opens Stripe's hosted payment page. There is no page of ours in between — no confirmation card, no billing form, no licence page. Everything the buyer must have read before paying therefore sits on the CARD, around the link (StorePlans.Offer / StorePlans.SubscribeTerms):

  1. the price, and the terms in one sentence — CHF 29 per month; we charge your card every month until you cancel, plus what a cancellation does — rendered from the LIVE plan's own price, so a re-priced plan cannot state one number and charge another;
  2. for a plan with a licence to accept, the clickwrap sentence: by subscribing you accept the licence of this plan, linking its text (Store/Licences/{Tier});
  3. the Subscribe link, /Store/Subscribe?plan={tier}&accept={hash} — the hash of the licence text the card linked to (Subscriptions.SubscribeLink).

/Store/Subscribe (PlanCheckout, a layout area on the store's catalog type; Subscriptions.CheckoutPath is the ONE definition the cards link to, the area is registered from, and the Stripe return URLs are built from) is then a HAND-OFF, not a page: for a signed-in buyer who may buy the plan it creates the plan order and redirects to the checkout URL the order control plane stamps (PlanCheckout.Decide → HandOff). An anonymous buyer is sent straight to sign-in, with the whole link — acceptance included — as the return URL, and lands on the payment page after. The card details and the billing address are taken on Stripe's page (billing_address_collection=required, StripeGateway.SubscriptionSessionFields), prefilled with the email of the buyer's billing profile when they have one.

🚨 Every outcome that is not a hand-off renders the Plans page, with the reason on the card of the plan it is about (StorePlans.View + PlanNotice) — a refusal, a failed order (the control plane's own Error), an abandoned payment (nothing has been charged), an acceptance the link did not carry. The buyer stays where the Subscribe link is, and the link is the retry; nothing is a dead-end card. A return from Stripe is never handed off again (an abandoned payment re-opening the page it was abandoned on is a loop). The two screens the surface keeps are both AFTER a purchase: the payment received notice Stripe returns to, and the MANAGE screen of the plan the viewer holds.

🚨 A buyer who comes back resumes, never re-orders. Because the hand-off happens on arrival, the browser's back button from Stripe, or a second click after abandoning the payment, would otherwise create a second order — which the per-buyer checkout claim rightly refuses while the first is open. The hand-off therefore reads the claim first and, when it names an order for the same plan, the same accepted text, still Created/PendingPayment and younger than 23 hours (Stripe's session lives 24), follows THAT order to its payment page (PlanCheckout.MayResume). An order in flight for a DIFFERENT plan is not resumed: the new order meets the claim and is refused, on the card, with finish or cancel it before starting another.

🚨 Paid, but not activated, is never sold again. The payment webhook stamps a plan order Fulfilled even when the activation fails, with the reason on OrderContent.Error (so a replay repairs it). That buyer holds no live plan, so no refusal fires, and a Fulfilled order used to release the checkout claim, which let a second checkout charge them twice. Now CheckoutClaim.PaidButNotActivated holds the claim whatever its age (CheckoutClaim.MayTakeOver), and the hand-off shows your payment was received, the plan is not activated yet, nothing more will be charged on the card. The repair (a webhook replay, or our team) clears the error and releases the claim.

A billed activation that replaces another tier also sets the record's status to None in the same patch. The watcher's Start()/Fail() carry status over, so an inherited Active would publish the PAID plan, with no expiry, before the activation ran, and for ever if it failed. Only Activated(...) publishes it.

Every line of CHROME comes from StoreTexts (EN + DE), including the renewal date, which is rendered through the viewer's zone (AccessService.ViewerZoneId → ChromeLocale.LongDate) and spelled in the resolved locale — a renewal date is the one number on the page a reader plans around, and a zone-less instant near midnight reads as the wrong day. The plan's highlights are the exception, deliberately: they are operator-authored prose on the tier node, and the store's rule is that authored content stays in the language it was written in (chrome is translated, prose is not).

The four guards

guard what it stops
the licence (OrderContent.AcceptedLicenceHash, PlanLicences) a subscription that starts on terms the buyer never saw. The Plans card states, beside the Subscribe link, that subscribing accepts the plan's licence and links its text (Store/Licences/{Tier}); the link carries the HASH of that text, the hand-off copies it onto the order, and a link carrying none (or one of a text that has since changed) is not handed off — the Plans page asks for the click again, on the card. A card whose licence cannot be read offers no link at all. SubmitPlan re-reads the licence as System BEFORE the claim and refuses when the order carries no acceptance, when the licence cannot be read, or when its text no longer hashes to what was accepted. The acceptance is then written as System to {buyer}/_LicenseAcceptance/plan-{tier} (core's LicenseAcceptance) BEFORE the Stripe session exists, and a write that does not confirm fails the order. Free (Apache-2.0) asks for nothing; the all-access plan is dedicated, and the legacy sme id resolves the same document.
the quote (OrderContent.QuotedAmount) the page and the control plane read the same node by the same rule, which makes them agree about the RULE and nothing about TIME. An operator repricing between the render and the click would state CHF 29 and charge CHF 39. What is charged still comes from the trusted node; a quote that no longer matches it is REFUSED — the only outcome that is neither a wrong charge nor a silent one.
the claim (CheckoutClaim, {buyer}/_Orders/_PlanCheckout) two tabs submitting before either completion webhook arrives BOTH read "no active plan" — a read cannot arbitrate a race, a CREATE can. The claim is taken before the Stripe session is created and released by EVIDENCE (the order it names went terminal, or 25 h passed — longer than a Stripe session lives), never by a release step a crash could skip.
the cadence (Subscriptions.IsSelfServeCadence) the order node is the buyer's own, so cadence: annual on a hand-made order would open a yearly subscription through a lane no page states the terms of — and both self-serve plans carry an annual price today. Refused, not normalized: quietly rewriting it would charge a different cadence than the order asked for.

The card and the hand-off refuse, before any order exists, what the order control plane also refuses at submit: a plan that is not self-serve, and a viewer who is already live on a paid plan. All three read ONE rule, Subscriptions.BlocksPlanPurchase:

the viewer's live record buying Personal / Pro
none, lapsed, or cancelled allowed
free — however it came to be Active 🚨 allowed. There is no card subscription to cancel and nothing to bill twice; the purchase REPLACES the record (below). Measured on the public instance 2026-09-26: a tier: free, status: Active record refused Personal with "Cancel it before subscribing" — cancel what?
the SAME plan refused — it is the manage screen, never a second purchase
another PAID plan on a card refused — a second checkout is a second Stripe subscription, and the buyer pays twice. Switching means cancelling the first at Stripe, which this lane does not guess at
another PAID plan GRANTED by our team (manual — a coupon's term, a comp) refused, for now. Nothing is double-billed, but replacing a grant silently throws away a term somebody decided on (a permanent staff plan, 90 days of a course coupon) — whether a purchase may supersede a grant is undecided, and until it is decided it is a conversation (write to us and we'll move you across)
an UNKNOWN tier (e.g. the legacy sme) refused: refusing costs a message, guessing wrong costs a double charge

Fulfilment replaces the record. checkout.session.completed activates through Subscriptions.RequestActivation → WithActivationRequested on the EXISTING record: the tier, the cadence (monthly) and the Stripe subscription id become the sold plan's, and permanence is cleared (a billed plan's expiry is the renewal's business). 🚨 A sold plan that replaces another tier starts its own clock: the watcher tops a billed activation up from a live expiry (early renewal), so a free record carrying a term would otherwise hand the paid plan that term plus a month — months nobody paid for. A billed activation onto a record of a DIFFERENT tier therefore drops the old validUntil in the same patch, and the paid month starts now; a renewal of the same tier still tops up.

The order

A plan order is an ordinary Store/Order at {buyer}/_Orders/{id} with planTier + cadence set instead of pluginPath (OrderContent.IsPlanOrder is the one discriminator), so the whole existing lane — buyer-owned node, requestedAction: Submit, the watcher, the checkout-URL redirect, the payment inbox — carries it unchanged. OrderControlPlane.SubmitPlan prices it SERVER-side off Admin/Tiers/{id} as System and asks Stripe for a mode=subscription session with an inline recurring price (no Stripe Product/Price administration: a re-priced plan needs a node edit only).

🚨 The price is read shape-tolerantly (Subscriptions.ReadPrice, the Parse pattern) rather than by binding TierContent, which is compiled only inside Store/Tier — binding it would drag that source into every consumer of the order layer. And an unreadable tier node refuses: the Plans page falls back to the shipped seeds because a pricing page must never be blank, but this is a CHARGE, and quoting a compiled-in number for a plan whose live terms could not be read is how a customer is billed a price nobody offers any more.

The recurrence

🚨 The facts are stamped TWICE at submit (OrderCheckout.PlanMetadata, stamped by the payments module's StripeGateway.SubscriptionSessionFields): on the SESSION, which checkout.session.completed hands back for the first activation, and on the SUBSCRIPTION via subscription_data[metadata], which every LATER event carries. Without the second copy the first renewal arrives naming nobody and the plan lapses after one month while the card keeps being charged.

Stripe event what it does here
checkout.session.completed (plan) activates Admin/Subscriptions/{viewer} on the monthly cadence, waits for the control plane to SETTLE it (Subscriptions.ObserveActivationSettled), then stamps the order Fulfilled + stripeSubscriptionId — with the failure reason in OrderContent.Error when the plan did not come out live. RequestActivation only FILES the request, so stamping in the same breath would report a plan we had merely asked for; a fulfilled order carrying an Error is the plan lane's "granted but never installed", and a webhook REPLAY re-runs it instead of no-oping. See One payment, one grant below for the two rules that keep this honest.
invoice.paid with billing_reason: subscription_cycle renews — the same Activate, which extends from the current expiry, so it tops up rather than restarting the clock
invoice.paid with billing_reason: subscription_create ignored. The opening charge is already granted by the checkout event; handling it too would give every new subscriber two months for one payment
customer.subscription.deleted files Cancel, which runs the revocation sweep
customer.subscription.updated whose previous_attributes names cancel_at_period_end or cancel_at records "will not renew" (or clears it when withdrawn) on the plan record — no entitlement change; see Cancelling
customer.subscription.updated, anything else ignored — the renewal flag did not change

The invoice's copy of the subscription metadata is looked for in several places on purpose — subscription_details.metadata up to API 2025-02, parent.subscription_details.metadata after it, and the line item's own. An account's API version is a dashboard setting, so pinning one is not ours to do; a reader that knew only one shape would stop renewing everybody the day it moved.

A month with no payment therefore lapses on its own: validUntil is not extended, and IsActiveAt stops new unlocks the moment it passes.

One payment, one grant — and no failure over a live plan

Measured on the public instance on 2026-09-26 (order rbuergi/_Orders/order-e2ab8e9e…, subscription sub_1UJxLY…): the checkout delivery arrived at 15:17:06, the plan went Done/Active at 15:17:09 — and the paid order was stamped "activating the plan did not finish" when the 45 s settle budget ran out. The same checkout was then fulfilled a SECOND time, which filed a second Activate (the record logs two [start] Activate … personal (monthly) runs naming the same order and subscription) and pushed validUntil two months out for one payment. Two defects, one fix set:

A wait that lapses names its cause; a run abandoned mid-flight is resumed

Two residual defects on the same path (MeshWeaver#5758), both in the SETTLE half — the fix set above made the activation exactly-once; these make a lapse diagnosable and stop one cause of it.

Cancelling

The checkout page is also the MANAGE screen: it says when the next charge falls due and carries the cancel button. 🚨 The button asks Stripe to stop renewing at the end of the period already paid for (cancel_at_period_end=true, never an immediate cancellation — the buyer paid for this month), and the entitlement here is ended only by the customer.subscription.deleted webhook that says Stripe actually stopped. Cancelling locally on the click would take a paying subscriber's access away while the card kept being charged, and would leave the two sides disagreeing if Stripe refused.

🚨 Once Stripe accepts, the request is RECORDED on the plan record (#3548): cancelAtPeriodEnd: true and cancelRequestedAt, beside status and never part of it — the plan stays Active and keeps covering what it covers until validUntil, exactly as a past-due mark does. Every surface reads it through SubscriptionFact.CancelAtPeriodEnd: the manage panel says "will not renew — active until …" instead of "renews on …", offers no second Cancel (Manage billing stays, which is where a cancellation is undone), and the Plans card's link reads Manage your plan. Before, the confirmation was transient layout state, so a reload said "renews on" again with no trace of the request anywhere. The write is authorised as the caller (the viewer's own panel, which read the subscription id from that very record, and Stripe has accepted it) and runs as System, because a viewer cannot write Admin. Only the record live on that subscription is marked, and RecordCancelRequested reports whether the record now carries the request: when it was replaced or ended while Stripe was being asked, nothing is marked and the panel says the cancellation could not be recorded (reload to see the current plan) instead of confirming it.

🚨 An activation does NOT clear it (#3593). A renewal (invoice.paid) for the period paid before the cancel can settle after it — still in flight when the plan page's cancellation succeeded, or delivered late — and the activation cannot tell that stale renewal from one after the subscriber resumed: both are processed after the cancel. Clearing on every activation erased a cancellation Stripe still held, and the page said "renews on" again. So Stripe's own renewal flag is the authority: its "renews again" report (customer.subscription.updated, below) clears the marker, the ending (customer.subscription.deleted) clears it, and an activation request that puts ANOTHER subscription behind the record — a new checkout, or a manual grant with none — drops it, because a marker belongs to the subscription it was recorded on. A renewal of the same subscription keeps it.

🚨 A cancellation made in the Stripe Customer Portal is recorded the same way (#3548, decided by the maintainer: subscribe to customer.subscription.updated). Stripe reports the request — and its withdrawal, when the subscriber changes their mind on the portal — as customer.subscription.updated. The Stripe provider turns it into the payment contract's renewal-state change (PaymentSubscriptionEvent.Renewal: CancelsAtPeriodEnd or Renews, the open vocabulary PaymentSubscriptionRenewal), and the inbox applies it with Subscriptions.RecordRenewalChange: "will not renew" sets cancelAtPeriodEnd + cancelRequestedAt, "renews again" clears both, and the panel follows. Three rules keep it honest:

customer.subscription.updated is therefore one of StripePaymentProvider.RequiredEvents. An endpoint registered before it was required lacks it, and the payment-path audit names the gap; the EnsurePaymentEndpoint maintenance task adds it (dry run, then execute: true with confirm set to the mode), keeping the signing secret — see Payments.

The cancel control is offered only when the record carries a stripeSubscriptionId. A plan GRANTED by the team (cadence manual) has no card subscription behind it, and a local cancellation for one would revoke a customer's access with nothing on the billing side having asked for it.

Managing the plan — one panel, three places

SubscriptionPanel is the ONE rendering of a plan the viewer holds: the plan and its price, the failed-payment banner, when it renews (or until when a granted plan runs), Manage billing and Cancel. It renders in three places: the viewer's profile page, as its Subscription section (Store/ProfileSections/subscription — a plain UiContribution node with context: Profile, address: Store, area: MyPlan, owner-only; its id is the section id, so the anchor is #profile-section-subscription); the standalone /Store/MyPlan (Subscriptions.ManagePath), which stays a working direct URL; and the checkout's own "your plan" screen. Every link is built by Subscriptions.ManageLink(viewer) and lands on the profile section: the Plans page ("You're on the X plan — manage it"), the post-payment screen (Manage your plan) and the billing portal's return URL. Only a link with no viewer to name (a sign-in return) points at /Store/MyPlan.

A failed charge — past-due, never revoked

invoice.payment_failed marks the record paymentStatus: PastDue (+ paymentFailedAt, the FIRST failure) through Subscriptions.RecordPaymentFailed, and only when the record is live on the failing subscription. 🚨 It is a field BESIDE status, never a new status value: every coverage reader — the paywall and the model-credit meter — grants on status: Active, and a failed charge does not end a period that is paid through validUntil while Stripe retries. The plan page and the Plans page show the banner ("Your payment failed — update your card") with Manage billing; the next invoice.paid (the successful retry of the same invoice, or the next cycle) activates the plan, which clears the mark; a Stripe that gives up sends customer.subscription.deleted, which ends the plan as usual.

One invoice, one period

A renewal carries its invoice as the grant key (invoice:{id}), so a redelivered invoice.paid is recognised by the record's control plane and extends nothing — the same exactly-once rule as the checkout (One payment, one grant above).

Not in this change (deliberately)

Access comes only through a plan (2026-09-30)

Maintainer: "no one should have any access, just through subs and tiers."

A plan that covers a package IS access — nothing is acquired (2026-10-10)

Maintainer: "i should not need to get anything for crying out loud. the sub should give me access", then "we do not want to get literally anything".

The rule (Store/Licensing/Source/PlanAccess.cs). When the INSTANCE's plan (Store:InstancePlan, a known tier; PlanAccess.InstancePlanOf) covers a package's tier, every signed-in user reads the package. No click, no hidden Get, no unlock record, no per-viewer grant, no notification. PlanAccess.ShapeOf(instancePlan, packageTier, audience, now) decides it once per package (a MALFORMED audience — not a list of strings — stays gated):

Shape When What the gating writes
Gated the instance's plan does not cover the tier, there is no instance plan, or the tier is blank or unknown today's paywall: Public + Anonymous deny on every gated child
OpenToSignedIn covered, no audience NO Public child deny (the root's Public Viewer grant inherits down); the Anonymous deny stays, so anonymous visitors see the cover only
Audience covered, and the root _Policy names an audience the gated shape plus one Viewer grant per audience id at {package}/_Access/audience-…_Access

Why the gating writes it, rather than the permission fold computing it. The rule's output is ordinary AccessAssignment rows. The C# permission fold reads them, and so does the PostgreSQL projection that search and listings filter on (partition_access, the per-schema rebuild). Search visibility therefore follows the read check, with no second implementation of the rule in SQL. The instance's plan is a fact about the instance, so the decision is per PACKAGE: no viewer is ever swept, and nothing is written when somebody opens anything.

Lapse. Every gating pass recomputes the shape (hub start, any real subtree change, an audience edit). An instance whose plan no longer covers the package gets its Public child denies back, and its audience grants are retracted. Configuration changes reach a running portal at its next start.

Audience. The audience is PartitionAccessPolicy.Audience (core) on {package}/_Policy, a list of strings with the wire name audience. Each entry is a USER id or a GROUP path; group membership counts transitively (GroupMembership, like the fold). Role ids are not subjects of the fold and are not supported. The reconcile never edits the audience; when it repairs the policy it re-writes the audience it read. An administrator's edit triggers a pass (PlanAccess.AudienceEdit); the reconcile's own policy write never does. With no audience, the behaviour is plan-only. Explicit grants (purchases, coupons, admin enrolments) still add on top.

The audience caps ALL plan-derived access. A legacy plan unlock (the old Get, or the Subscribe page's arrival unlock) minted a package-wide {viewer}_Access grant that an audience would not narrow. Once the instance's plan covers the package those grants are redundant, so the gating retracts every one that the records mark as plan-derived. The records are the plan-… row in Admin/Purchases or the subscription's unlock record. A grant that an ETERNAL marker ({package}/_Entitlements/{viewer}: a purchase, a coupon, an admin grant) also backs is never touched, audience or not, and neither is a hand-made grant with no plan record. On a covered package Subscriptions.Unlock mints nothing and the paywall offers no plan unlock, so a viewer outside the audience sees the ordinary paywall and cannot re-acquire around it.

What a covered viewer sees. The cover, the CTA row and the paywall treat a plan-granted viewer as an owner (PlanAccess.CoveredOrEntitled): straight into the content, "Open", never Get or Subscribe. "Your plan" on /Store/Plans reads the EFFECTIVE plan and says "Your plan: Enterprise — this instance's plan." (it used to show nothing for an instance plan). An uncovered viewer sees exactly the paywall, plans, coupon and contact they saw before.

API for other surfaces (the app launcher).

Provenance: a plan grant is always told from a hand-made one. A grant the plan path mints must be retractable with the plan, so it needs a record that says the plan minted it. The access model's own mark only goes halfway: a grant written by the platform carries createdBy = System (the field core's access-grant notifier reads to tell an entitlement from a person sharing something), which separates it from a hand-made grant but not a plan unlock from a purchase. The Store's records do the rest, and the rule is that the record is written BEFORE the grant (Subscriptions.ProvenanceWrites):

The grant was minted by Its record, written first What reads it
a purchase, a coupon, an admin grant the eternal marker {package}/_Entitlements/{viewer} never retracted
an unlock under a subscription record the unlock record Admin/Subscriptions/{viewer}/{package} the cancellation sweep; the gating
an unlock under an IMPLICIT plan the plan-{package}-{viewer} row in Admin/Purchases, naming the plan the gating (coverage and lapse)

The third row is the one that used to be missing. An implicit plan has no subscription node to file an unlock record under, so the unlock wrote nothing first and appended its ledger row afterwards, best-effort: a failed append was swallowed and left a package-wide grant with no record at all. Such a grant cannot be told from a hand-made one, so nothing retracts it and it outlives an audience restriction and a plan lapse. The ledger row now takes the mandatory first slot; if it cannot be written, no grant is minted.

What retracts an implicit-plan unlock. The gating pass reads the provenance both ways (PluginGate.SeedGating):

Both run whenever the package's gating pass runs: on its hub's start (an instance-plan change is a configuration change, so a restart) and on any change in its subtree (a tier change is one).

The grants that predate the rule: MarkPlanGrants. A Store/Maintenance task (Store/Licensing/Source/PlanGrantBackfill.cs) classifies every package-wide viewer grant (PlanAccess.ClassifyRootGrant):

Class Decided by A real run
Eternal the eternal marker exists (decided first) leaves it alone
PlanDerived a plan record exists leaves it alone
UnmarkedPlanUnlock no record; written by System and never edited by a person; exactly the unlock's roles (Viewer, with or without Commenter); created on or after the day the implicit unlock shipped files the missing plan-… row
Unclassified anything else: a hand-made grant, a wider role, an older grant, a lost stamp leaves it alone

The third class is safe to infer because every OTHER System writer of a root grant writes the eternal marker before it (Entitlements.GrantFrom), so a System grant with no marker is the plan path's. To run it, an administrator creates a Store/Maintenance node under Admin/Maintenance:

{ "task": "MarkPlanGrants", "package": "Underwriting", "requestedAction": "Run" }

package is optional (blank = every package). That is a dry run: it writes the reviewable report to {request}/Report — the counts per class, every grant with its class, and every read that failed — and marks nothing. A real run adds "execute": true, "confirm": "MARK-PLAN-GRANTS". It is refused while any part of the inventory could not be read (a subset is not the grant set), re-reads each grant at the moment it marks it, deletes nothing and changes no grant itself. The mark lands in Admin/Purchases, outside the package's subtree, so it does not wake the package's gating watcher: the run therefore runs the ordinary gating pass on every package in which a mark landed, and a pass that fails makes the run's summary a ❌ naming the package. That pass then treats the marked grant like every other implicit-plan unlock (above): retracted where the instance's plan covers the package, so a viewer inside the audience keeps reading through the plan, and retracted where the plan no longer covers it.

Not in this change. A PERSONAL plan above the instance's plan (a Store/Subscription record) is a fact about one viewer. It still goes through the Subscribe surface's arrival unlock (Subscriptions.Unlock, revocable on lapse). Expressing it as data, for example as plan groups that the subscription control plane maintains, is the follow-up that removes that last automatic Get. On an instance that states NO plan, the free plan every account holds still acquires per viewer. Opening every free package to every account there is a separate decision.