feat: sell status pages as a per-instance licence feature
Chart Release / chart (push) Successful in 13s
Server Deploy / deploy (push) Successful in 9m30s

This commit is contained in:
2026-08-25 09:25:05 +00:00
parent 68160dc681
commit 28b813ba64
4 changed files with 11 additions and 3 deletions
+2
View File
@@ -949,6 +949,8 @@ Admin's own database is separate and holds `accounts` · `admin_instances` · `l
`plans` is keyed on `(deployment, tier)` — six rows, two deployments times three tiers — and holds base allowances only. **Every Paddle price ID lives in `catalogue`**, one row per priceable component (`base`, `limit`, `feature`), because a metered plan is priced by several prices and one map on a plan row cannot express that. `entitlements` holds one row per instance with `desired` beside `granted`: the checkout is built from `desired`, a licence is only ever signed from `granted`, and an abandoned checkout therefore leaves a `desired` that reached nothing. The two Free plans have **no catalogue rows at all**, which is what keeps Free outside Paddle.
**No tier bundles a feature.** `console`, `oidc`, `vuln_scanning` and `status_pages` are each a per-customer priceable add-on: every plan row carries an empty `base_features`, and the grant comes from a `catalogue` row the customer buys. Adding a fifth feature therefore means one more `KindFeature` row per paid plan in `SeedCatalogue` and one entry in `adminsite/lib/features.ts` — that map is what the customer's grant list, the staff configurator and the purchase form all enumerate, so a feature missing from it exists in the licence and is invisible in the portal. `SeedCatalogue` upserts on `(kind, deployment, tier, feature_key)`, so a new row reaches an existing database on the next admin boot with no migration; `SeedPlans` is `$setOnInsert` on the whole document and would not, which is the other reason bundling into a tier is the harder path.
### Migrations
`services.RunMigrations()` runs at boot, recording markers in `migrations`:
+5 -2
View File
@@ -67,8 +67,10 @@ func (r CatalogueRow) Priced(env string) bool {
return false
}
// SeedCatalogue inserts the twenty rows the four PAID plans need: a base, a
// server limit, and one row per feature key.
// SeedCatalogue inserts the twenty-four rows the four PAID plans need: a base, a
// server limit, and one row per feature key. The count is deliberate — it moves
// whenever shared/license gains a feature, and this comment is how the next
// person knows the number was chosen rather than drifted.
//
// The two Free plans get no rows at all, and that absence is what keeps Free
// outside Paddle: with nothing to price, no checkout can be built for it. Do not
@@ -86,6 +88,7 @@ func SeedCatalogue(ctx context.Context) error {
{Kind: KindFeature, Deployment: deployment, Tier: tier, FeatureKey: license.FeatureConsole},
{Kind: KindFeature, Deployment: deployment, Tier: tier, FeatureKey: license.FeatureOIDC},
{Kind: KindFeature, Deployment: deployment, Tier: tier, FeatureKey: license.FeatureVulnScanning},
{Kind: KindFeature, Deployment: deployment, Tier: tier, FeatureKey: license.FeatureStatusPages},
}
for _, r := range rows {
filter := bson.M{
+2
View File
@@ -10,12 +10,14 @@ export const FEATURE_LABEL: Record<string, string> = {
console: "Browser console",
oidc: "Single sign-on",
vuln_scanning: "Vulnerability scanning",
status_pages: "Status pages",
};
export const FEATURE_DESC: Record<string, string> = {
console: "In-browser SSH, RDP and VNC sessions",
oidc: "OIDC sign-in for your whole team",
vuln_scanning: "Package inventory matched against distribution security advisories",
status_pages: "Public status pages for your customers, built from your monitors",
};
export function featureLabel(key: string): string {
@@ -28,13 +28,14 @@ entitlement.
## Features
Three features are enabled per instance rather than bundled into a tier:
Four features are enabled per instance rather than bundled into a tier:
| Feature | What it enables |
| ---------------------- | ------------------------------------------------------------------------------- |
| Browser console | The [browser console](../vantage/browser-console.md) |
| Single sign-on | [Sign-in through your identity provider](../vantage/settings.md#single-sign-on) |
| Vulnerability scanning | [Package vulnerability scanning](../vantage/vulnerabilities.md) |
| Status pages | [Public status pages](../vantage/status-pages.md) |
No tier includes them by default; you enable them on the instances that need
them.