Changelog — webspresso/plugins/polar

1.0.1 — Unreleased #

Fixed #

  • package exports: webspresso/core/auth, webspresso/plugins/polar, and webspresso/plugins/polar/src/* resolve without manual /index.js suffix
  • Rate limiting: Polar limiters use express-rate-limit v8 ipKeyGenerator (fixes production startup ValidationError)
  • Webhook user lookup: metadata.user_id supports nanoid/UUID string primary keys
  • CSP: polarCspDirectives() includes 'self' in formAction and connectSrc

Added #

  • hooks.isPaidUser({ user, knex, config }) for external tier tables (legacy (user, knex, config) still supported)
  • syncBeforeCheckout option (default true)
  • polar.api.syncBillingForAppUser(user, knex, { hooks }) for dashboard page-load sync
  • mergePolarCspDirectives(existing) for Helmet CSP union merge
  • Production checklist: docs/guides/polar-production-checklist.md
  • Integration tests: rate-limit startup, IPv6 key validation, isPaidUser hook

1.0.0 — 2026-09-10 #

First production-ready release.

Added #

  • Zero-dependency Polar SDK (crypto + fetch): webhook verification, API client, checkout, portal, Customer State sync
  • Auto routes: webhook, checkout (GET/POST), portal, billing status JSON API
  • Idempotent migration generator + webspresso polar:migrate CLI
  • Configurable field mapping, tier/plan mapping, hooks (onSubscriptionChange)
  • Optional polarSyncMiddleware for page-load tier refresh
  • Nunjucks dateLabel filter + Date-safe truncate wrapper
  • CSP helper (polarCspDirectives) for checkout form redirects
  • Zod env schema export (parseEnv, polarEnvSchema)

Production lessons encoded #

IssueFix
Checkout URL is often https://polar.sh/checkout/polar_cst_*, not only buy.polar.shextractCheckoutUrlFromResponse + client_secret builder
https://polar.sh/purchases/subscriptions → 404Blocked in URL validators
subscription.created with incomplete statusFallback Customer State sync before granting Pro
Metadata may not propagate to subscriptionexternal_customer_id required on checkout; multi-strategy user resolver
polar_current_period_end as Date breaks Nunjucks truncateAlways persist ISO strings; dateLabel + safe truncate
Checkout "already subscribed" API errorPre-checkout syncPolarBillingForUser + early redirect
Webhook 401Standard Webhooks HMAC (webhook-id, webhook-timestamp, whsec_ decode); document raw body requirement
Sandbox vs production OATPOLAR_SANDBOX=true → sandbox-api.polar.sh
Empty metadata fields → Polar 422buildCheckoutMetadata omits blank values
Portal deprecated pathsValidate /{org}/portal only; fallback POST /v1/customer-sessions/

Migration notes for existing custom Polar integrations #

  • Remove local utils/polar.js and file routes under billing/webhook paths — plugin registers equivalent routes
  • Run webspresso polar:migrate (or plugin.api.generateMigration()) once
  • Mount createWebhookRawBodyMiddleware('/api/v1/polar/webhook') before express.json() in custom servers for strict signature verification
  • Wire hooks.onSubscriptionChange if tier updates need side effects (limits, analytics, etc.)
Updated