CRUMB a card from devarno-cloud

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

SurfacePathPurpose
Adapterconsole/src/lib/polar/entitlements.tsthe only file Polar.sh code is allowed to live in
Webhook/api/relay/polarmutates 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_GATE

What the webhook actually does

/api/relay/polar (Phase 7) is the only ingress for Polar webhooks. It:

  1. Verifies the Polar webhook signature.
  2. Resolves the workspace via Polar’s customer_external_idslug.
  3. On subscription.activeDriver.Upgrade(ref, profile_for_new_tier).
  4. On subscription.canceled → check data vs. caps, transition to read_only or downgraded.
  5. 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_BILLINGenabled (cloud) or disabled (self-host); when disabled the Polar adapter compiles to a no-op
  • tier_downgrade_active_data → policy: workspace exceeds new caps → read-only 7 days → HATCH alert → admin decommission
  • assertEntitlement → single seam in entitlements.ts called 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