title: Phase 7c-c-b close — Polar SDK replaces entitlement env stub date: 2026-07-03 status: Accepted
Context
Phase 7c-c-a (2026-06-29) landed producer-side entitlement + approval seams with env-driven stubs. The entitlement stub read ROCKY_POLAR_TIER_<slug> + ROCKY_POLAR_SEATS_<slug> — sufficient to gate producers behind a shape, but not connected to real subscription state. Phase 7c-c-b's contract from that plan: swap the env stub for a real Polar.sh SDK integration, land the spec-pinned single home at console/src/lib/polar/, and wire a webhook so the customer cache stays fresh without synchronous Polar reads on the hot path.
Decision
Six-file src/lib/polar/ module (mode, products, customers, client, entitlements, webhook) plus the POST /api/relay/polar webhook route replaces the env stub. src/lib/agent/entitlement.ts shrinks to a re-export shim so producer imports (@/lib/agent/entitlement) keep resolving without a churn PR across 6 subsystems. Both existing call sites (agent/gate.ts, hearth/service.ts) become async on the entitlement path.
Key shapes:
ROCKY_BILLING=disabledis the OSS-parity switch. Default isdisabled. In disabled modeassertEntitlementreturns{allowed: true}without touching disk or Polar; the webhook route still verifies its signature and processes events (a self-host can still drive the cache manually for testing), but a self-host that never talks to Polar never faults a gate. Redesign spec §Polar.sh integration: "the Polar adapter compiles to a no-op when disabled". This is not a build-time exclusion (that class of dead-code elimination is Node-hostile) but a runtime short-circuit before any Polar-flavored code path runs.The customer store is a cache, not the source. Polar is authoritative for subscription state. The
polar-customers.jsonfile records what we last heard on a webhook.assertEntitlementreads only the cache — a Polar API outage or slow-response must not fault a producer invocation. The webhook is the only writer of{tier, seats, status}for a workspace.Workspace binding lives Polar-side, via
metadata.workspace_slug. No rocky-side "link" UI in this phase — a webhook withoutmetadata.workspace_slugwarns + no-ops. Checkout wiring, admin binding UI, and metadata reconciliation are future slices. Deliberate choice: keeping the binding in the Polar object model means we don't need a separate CRUD surface + persistence layer for the mapping.subscription.canceledflips status but does NOT downgrade tier. Redesign spec §229 requires a 7-daytier_downgrade_active_datagrace with read-only mode. That policy needs a boot-time sweep + cron and is filed as7c-c-b-followup. Today the gate simply denies with reasonsubscription canceled— safer to over-deny for the followup window than to under-deny and lose data guarantees.Webhook
POLAR_WEBHOOK_SECRETunset → 401 on every request. Fail-closed.verifyWebhookSignaturereturnsfalseon empty secret regardless of what the caller sent. Symmetric to howAIRLOCK_JWKS_URLunset would fail the callback route in 7c-c-c — untrusted input is untrusted unless we can prove it isn't./api/relay/polaris public in middleware. Polar posts server-to-server; there is no operator session. The route body's signature verify is the only gate. Same shape as/api/health(auth-free by design).
Consequences
- Producers keep the same 402 deny reason strings for tier/seats denials — the 7c-c-a fanout call sites don't change beyond adding
await. - New 402 shapes:
"no polar customer bound to workspace"(cloud + not yet bound),"subscription canceled","subscription past_due". Producer UI (workbench toasts etc.) needs to render these; today they surface as the rawbody.errorstring. - Test infrastructure grows a
polar-customers.jsonseed step for producer tests that exercise deny paths (ralph/runs,relay,sniffer/analyze). The oldROCKY_POLAR_TIER_<SLUG>env override is gone — cloud mode + seeded cache row is the new pattern. - Follow-ups queued:
7c-c-b-followup(7-daytier_downgrade_active_datagrace), admin binding UI, hearth wrapper Polar tier vocabulary mapping (redesign spec §Polar.sh: solo/team/fleet/enterprise vs hearth's solo/team/studio/bespoke).
Related
- Predecessor plan:
docs/plans/2026-07-03-phase-7c-c-b.md - Phase 7c-c-a close:
docs/decisions/2026-06-29-phase-7c-c-a-close.md - 7c-c-c consumer close:
docs/decisions/2026-07-02-phase-7c-c-c-consumer-close.md - Redesign spec:
docs/specs/2026-05-02-rocky-system-redesign.md§Polar.sh integration