Skip to content

subscription-paddle

Paddle Billing provider plugin for billing-foundation. Paddle acts as merchant of record: it sells to the customer and issues invoice and VAT, so the consumer-protection texts for this provider come from consumerProtection.providerTexts. Reads its API key, webhook secret and tenant binding key from system config keys with backing:"secrets" (envelope-encrypted in the secrets store under the system tenant), the public Paddle.js client-token, a billingLive master switch and an environment key (sandbox | live). environment is its own key because Paddle serves sandbox and live from different API hosts and a live key must never hit the sandbox host or the other way round; billingLive only decides whether checkouts are offered at all. All of them are runtime config, so keys rotate and prod goes live without a redeploy, and the mask on each derives the sysadmin settings screen and nav. Mount via createSubscriptionPaddleFeature({ priceToTier }); the optional apiKey/webhookSecret/tenantBindingKey/clientToken options are env to config bridge fallbacks (PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, PADDLE_TENANT_BINDING_KEY, PADDLE_CLIENT_TOKEN). The plugin always mounts; createCheckoutSession throws feature_disabled unless billingLive is true, and billing counts as enabled only with API key and binding key present. Webhooks arrive at /webhooks/subscription/paddle when mounted through the server-runtime billing.paddle boot option, or at /api/subscription/webhook/paddle when createSubscriptionWebhookRoute is mounted by hand and are verified with the Paddle-Signature header (HMAC-SHA256 over ts:rawBody keyed with the raw secret, any h1 may match, 5 minute tolerance). Paddle.js can open checkouts with arbitrary customData from the browser, so custom_data.tenantId is only trusted together with tenantBinding, an HMAC of the tenant id under the binding key; events without a valid binding, for prices outside priceToTier or without a tenant are dropped. Rotating the binding key makes the webhooks of existing subscriptions fail that check until they are re-bound. Paddle has no hosted checkout page, so the feature serves the Default Payment Link itself at /billing/paddle-checkout (the Default Payment Link in the Paddle dashboard must point to <APP_BASE_URL>/billing/paddle-checkout; it is the only source, because checkouts are created without a checkout.url). Create subscription prices in the Paddle catalog with quantity minimum and maximum 1; for one-off packs set the quantity limits on purpose, because the payment event carries quantity and the app has to multiply its credits by it: an anonymous page with its own CSP that loads Paddle.js from cdn.paddle.com, the one external script this framework allows, and opens the overlay for the transaction. Implements checkout, customer portal, cancel and resume, retrievePrices (batched, 10-minute TTL cache), retrieveSubscription for the sync job, and an in-app plan switch: previewPlanSwitch shows the proration before the customer confirms, applyPlanSwitch changes the plan without a hosted page and waits for the subscription.updated webhook to move the tier.

What this feature needs to run (Requires).

flowchart TB
  n_subscription_paddle["subscription-paddle"]
  subgraph how_reqs["Requires"]
    n_billing_foundation["billing-foundation"]
    n_config["config"]
    n_secrets["secrets"]
  end
  n_billing_foundation --> n_subscription_paddle
  n_config --> n_subscription_paddle
  n_secrets --> n_subscription_paddle

Per-tenant config keys, set via the tenant-admin UI or a seed. 🔒 = encrypted at rest.

KeyTypeDefaultScopeWho can writeWho can read
api-keytext—system 🔒SystemAdminTenantAdmin, Admin, SystemAdmin
billing-livebooleanfalsesystemsystem, SystemAdminTenantAdmin, Admin, SystemAdmin
client-tokentext—systemSystemAdminTenantAdmin, Admin, SystemAdmin
environmentselect (sandbox | live)sandboxsystemSystemAdminTenantAdmin, Admin, SystemAdmin
tenant-binding-keytext—system 🔒SystemAdminTenantAdmin, Admin, SystemAdmin
webhook-secrettext—system 🔒SystemAdminTenantAdmin, Admin, SystemAdmin
  • Registers extension: subscriptionProvider → paddle