Implements spec 7 tasks 2-10 on top of the six-plan payload from task 1. Admin: plans re-keyed on (deployment, tier); new catalogue collection holds every Paddle price ID (one row per priceable component); new entitlements collection holds desired beside granted. admin/internal/catalogue owns both folds — entitlement to licence limits, and entitlement to Paddle line items — so the base allowance is subtracted in exactly one place. licensing.Issue now snapshots the instance's granted entitlement, never desired. Free is enforced per account AND deployment. Staff endpoints for plans, catalogue and entitlements; Free self-hosted can be claimed and renewed on its annual term; the reaper stays cloud-only. Server: enforces the monitor cap, audit-log retention (daily sweep, skips Unlimited and lapsed instances), and gates the OIDC callback. Unset limits are filled from the seed plan at the single decode site so old blobs never read as zero. Frontends: adminsite gains a catalogue price-ID editor, six-plan allowance screen, and a catalogue-driven PlanConfigurator mounted on the staff instance page. web shows monitors, audit retention and support level on the licence page. Docs: CLAUDE.md, spec index and plan 5 preamble updated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vantage Licensing Programme — Spec Index
Build in this order. Specs 0a–5 were designed 2026-07-24; spec 6 on 2026-07-26.
| # | Spec | Plan | Status |
|---|---|---|---|
| 0a | shared-module | plan | shipped |
| 0b | instance-rename | plan | shipped, migration verified on live |
| 1 | licensing-core | plan | shipped |
| 2 | instance-licensing | plan | shipped, no grandfathering — existing cloud instances are read-only until admin backfills |
| 3 | admin-backend | plan | shipped, verified end to end against scratch databases |
| 4 | admin-site | — | ready to start |
| 5 | paddle-billing | plan | ready to start, but revised by 7 — its "signup migration off sitesvc" section is superseded by 6, and its single-price-per-subscription assumption by 7 |
| 6 | cloud-instance-creation | — | ready to start |
| 7 | metered-licensing | plan | shipped — staff can configure and issue any of the six plans; no customer can buy one until 5 lands |
Specs 1 and 2 together give working licensing with licences cut by hand with
lkctl — no admin service needed. 4 and 5 can run in parallel once 3 lands.
7 lands before 5. It re-keys plans on (deployment, tier), moves every Paddle
price ID out of plans into a new catalogue collection, and adds the
entitlements collection that both a subscription and a licence are derived from
— all of which plan 5 builds on top of, so building 5 first would mean writing
its billing code twice.
The shape
Account (admin only)
├── Instance 1 cloud vantage.hostxtra.co.uk/<slug> licence auto-injected
├── Instance 2 cloud licence auto-injected
└── Instance 3 self-hosted customer's own deployment licence pasted by hand
The control plane knows only Instance. Accounts exist solely in the admin service, because a self-hosted instance has no row in the cloud database at all.
Decisions that everything else follows from
Licences are offline-verified signed blobs. ECDSA P-384 with SHA-256 via
github.com/hyperboloide/lk, public key compiled into the server, no phone-home
anywhere. This buys air-gapped self-hosting and means no Vantage instance ever
depends on the licensing service being up. It costs revocation: a licence is
valid until it expires whatever Paddle later says. Self Hosted is annual-only to
bound that window.
Every licence is bound to one instance UUID. Self-hosted customers link their UUID before the licence is signed, so there is no unbound licence and no claim protocol.
Expiry degrades, it does not break. Monitors keep executing, alerts keep firing, agents keep their keys, in-flight workflow runs finish. Mutations stop. Deletes and OS-update application stay open so a customer is never trapped over-limit or unpatched.
Tiers are data, not code. The server reads Limits and Features and never
branches on tier name. Tier contents live in the admin plans table and are
snapshotted into each issued licence, so editing a plan never rewrites history —
the same rule as workflow_runs.steps_snapshot.
Spec 7 replaces the three-tier table below with two deployments times three tiers, and makes the server count a metered quantity rather than a fixed allowance. See metered-licensing for the current grid. As shipped through spec 3, the table is:
| Free | Professional | Self Hosted | |
|---|---|---|---|
| deployment | cloud only | cloud | self-hosted |
| max servers | 3 | unlimited | unlimited |
| max secret groups | 1 | unlimited | unlimited |
| max channels | 1 | unlimited | unlimited |
| console | no | yes | yes |
| OIDC | no | yes | yes |
| term | monthly, £0 | monthly or annual | annual only |
Free is cloud-only by construction: it is only ever signed with
deployment: "cloud", and verification rejects a deployment mismatch. There is
no server-side flag to edit. One Free instance per account.
Spec 7 ends that construction-level guarantee — there is a self-hosted Free
plan, so plan.Deployment != inst.Deployment no longer implies it, and the Free
limit becomes one per account per deployment.
Existing cloud tenants are not grandfathered. The migration that would have
done it was removed before plan 2 shipped, so every existing cloud instance is
read-only until it is licensed by hand through the admin service: attach it to an
account with POST /api/staff/instances, then POST /api/staff/instances/:id/issue.
That flow is verified in plan 3, so it works today via the API and is the first
job the admin UI is used for.