Polar Entitlements & OSS Parity
rocky intermediate 5 min read
ELI5
Polar.sh is the cloud-only billing layer; it is one isolated file in the console and a webhook into RELAY. Self-host installs compile that file to a no-op so OSS users never sign in to Polar, never link its code, and never break on a cancelled subscription. When a subscription cancels but the workspace is bigger than the cheaper tier allows, the system enters a loud read-only grace period rather than silently deleting data.
Technical Deep Dive
One file, one route
| Surface | Path | Purpose |
|---|---|---|
| Adapter | console/src/lib/polar/entitlements.ts | the only file Polar.sh code is allowed to live in |
| Webhook | /api/relay/polar | mutates HEARTH tier on subscription.active / subscription.canceled |
Self-host build sets ROCKY_BILLING=disabled and forces tier=solo. The Polar adapter compiles to a no-op when disabled. No Polar code paths leak into self-host installs.
When entitlement is checked
Per the tenancy invariant point (3) (rocky-003): Polar.sh is checked only on provisioning, upgrade, and decommission of any tier > solo. Per-request runtime checks are not required — tier is enforced through the persisted DeploymentRef row plus the tier_downgrade_active_data policy below.
The downgrade policy (tier_downgrade_active_data)
System-redesign §Error handling, with the strongest rule in the matrix:
Polar cancels, workspace exceeds new caps → Read-only mode 7 days, then HATCH alert + admin decommission flow — never silent data loss.
stateDiagram-v2 [*] --> active: subscription.active active --> read_only: subscription.canceled AND data > caps active --> downgraded: subscription.canceled AND data <= caps read_only --> alerted: 7 days elapsed alerted --> admin_decommission: admin acts admin_decommission --> [*] downgraded --> [*]Build-mode separation
flowchart TB subgraph SH[Self-host build] SH_ENV[ROCKY_BILLING=disabled] SH_ADP[entitlements.ts → no-op] SH_TIER[tier forced to solo] end subgraph CB[Cloud build] CB_ENV[ROCKY_BILLING=enabled] CB_ADP[entitlements.ts → live Polar SDK] CB_HOOK[/api/relay/polar webhook/] CB_GATE[gate at provision/upgrade/decommission] end SH_ENV --> SH_ADP --> SH_TIER CB_ENV --> CB_ADP CB_ADP --> CB_GATE CB_HOOK -->|webhook event| CB_GATEWhat the webhook actually does
/api/relay/polar (Phase 7) is the only ingress for Polar webhooks. It:
- Verifies the Polar webhook signature.
- Resolves the workspace via Polar’s
customer_external_id→slug. - On
subscription.active→Driver.Upgrade(ref, profile_for_new_tier). - On
subscription.canceled→ check data vs. caps, transition toread_onlyordowngraded. - Writes a HATCH event before any HEARTH mutation — same tenancy-invariant ordering as every other Rocky route.
The “no leak” enforcement
Phase 5 D9 explicitly defers Polar to Phase 7 and compiles the entitlement check to a no-op stub in 5d:
Tenancy invariant point (3) compiles to a no-op in self-host. Implementation lands in Phase 7; the seam is a single
await polar.assertEntitlement(slug, tier)line that’s a no-op stub in 5d and gets a real implementation later.
Phase 5e CI runs the OSS-parity e2e (rocky-011) with ROCKY_BILLING=disabled — required-for-merge — so a regression that links Polar code into the self-host bundle fails CI before it lands.
Key Terms
ROCKY_BILLING→enabled(cloud) ordisabled(self-host); when disabled the Polar adapter compiles to a no-optier_downgrade_active_data→ policy: workspace exceeds new caps → read-only 7 days → HATCH alert → admin decommissionassertEntitlement→ single seam inentitlements.tscalled by HEARTH routes; the line that becomes a no-op in self-host- OSS-parity invariant → the Phase 5e e2e proves no Polar network call happens in
solo+LocalDocker+LocalAuth
Q&A
Q: Why is Polar checked only on lifecycle events instead of per-request?
A: Tier is already enforced by the persisted DeploymentRef row, and the downgrade-active-data policy gives a loud 7-day grace before any data action. Per-request checks would add latency without buying any safety once the row is gated.
Q: What guarantees self-host installs don’t link Polar code?
A: entitlements.ts is the single allowed location; the call sites use await polar.assertEntitlement(...) against a no-op stub when ROCKY_BILLING=disabled. The OSS-parity e2e in Phase 5e CI runs in this exact mode and is required-for-merge.
Q: What happens to a workspace whose subscription cancels but holds more data than solo allows?
A: It enters read_only for 7 days, then HATCH emits an alert and the admin decommission flow is required. Never silent data loss.
Examples
A streaming service with a “downgrade” button: cancelling a Family plan with five active profiles does not delete the four extras. The account goes read-only for a week, the admin gets a notice, and someone has to choose which profiles survive. The free tier just doesn’t have the buttons that would call the billing system at all.
neighbors on the map
- Billing Architecture debugging why a user's tier did not update after payment
- OSS & Cloud Modes self-hosting via docker compose