CRUMB a card from devarno-cloud

Polar Billing Webhook Flow

skyflow intermediate 6 min read

ELI5

Skyflow doesn’t process payments — Polar.sh does. Skyflow creates a checkout session, redirects the user, and then waits for Polar to call POST /webhooks/polar. The webhook handler verifies the HMAC signature, upserts the subscriptions row, and re-publishes the event onto NATS so the rest of the system reacts.

Technical Deep Dive

Checkout Initiation

sequenceDiagram
autonumber
participant C as Client
participant API as core-api
participant P as Polar API
participant PG as Postgres
C->>API: POST /api/v1/billing/checkout {tier, interval}
API->>API: assert user not already on tier
API->>P: create checkout session
P-->>API: {checkout_url, session_id, expires_at}
API->>PG: INSERT checkout_sessions (polar_session_id UNIQUE, status='pending')
API-->>C: {checkout_url, session_id, expires_at}
C->>P: browser → Polar hosted checkout

Webhook Sequence

sequenceDiagram
autonumber
participant P as Polar
participant API as core-api /webhooks/polar
participant PG as Postgres
participant N as NATS SUBSCRIPTION
participant U as USER stream
P->>API: POST signed payload
API->>API: verify HMAC (POLAR_WEBHOOK_SECRET)
alt signature invalid
API-->>P: 401
else valid
API->>API: parse event_type
alt checkout.completed
API->>PG: UPDATE checkout_sessions SET status='completed', completed_at=now()
API->>PG: INSERT subscriptions (polar_subscription_id UNIQUE, status='active')
API->>N: publish events.subscription.created
API->>U: publish events.user.tier_changed (UserTierChangedEvent)
else subscription.active
API->>PG: UPDATE users SET tier=:new_tier
else subscription.canceled
API->>PG: UPDATE subscriptions SET cancel_at = current_period_end
Note over API,PG: 7-day grace, downgrade scheduled at period_end
API->>N: publish events.subscription.canceled
else subscription.payment_failed
API->>N: publish events.subscription.payment_failed
Note over API: triggers events.realtime.tier_limit_warning later
end
API-->>P: 200 {received: true}
end

Idempotency Surfaces

LayerMechanism
subscriptions.polar_subscription_idUNIQUE — ON CONFLICT update only
checkout_sessions.polar_session_idUNIQUE
Webhook re-deliveryPolar retries on non-2xx; HMAC + UNIQUE keys keep it safe
Downstream NATSBaseEvent.event_id consumers dedupe

Cancellation Grace

subscription.canceled does not flip status to canceled immediately. It sets cancel_at = current_period_end. A scheduled job downgrades the user only after current_period_end passes (the spec calls this a 7-day grace window). The partial unique index one_active_subscription_per_user allows a future new active subscription once the row flips.

Failure Flow

flowchart TD
P[Polar fires webhook] --> V{HMAC valid?}
V -->|No| L[401 + log]
V -->|Yes| H[handler]
H --> R{Postgres write OK?}
R -->|No| E[500 → Polar will retry]
R -->|Yes| N[publish to NATS]
N --> A[200 ack]

Key Terms

  • HMAC (Polar) → header signature derived from POLAR_WEBHOOK_SECRET; mandatory verification before any DB write
  • polar_subscription_id UNIQUE → idempotency anchor; replays are safe upserts, not duplicates
  • Cancellation grace → cancel-at-period-end; service continues until billing period ends
  • UserTierChangedEvent → secondary event so non-billing services (gamification, realtime) update their views

Q&A

Q: How is the webhook authenticated? A: HMAC signature in a Polar-provided header, verified against POLAR_WEBHOOK_SECRET. Failure returns 401 before any state mutation.

Q: What happens when Polar retries a webhook? A: The unique constraints on polar_subscription_id and polar_session_id make the writes idempotent. Downstream NATS consumers further dedupe on BaseEvent.event_id.

Q: Why doesn’t subscription.canceled immediately downgrade the user? A: The user has paid through current_period_end. A scheduled job demotes the tier only after that timestamp; meanwhile they keep their paid features.

Q: What does the user see if their card declines mid-cycle? A: events.subscription.payment_failed triggers an email (subscription-notifier) and queues events.realtime.tier_limit_warning for the in-app notification surface; the tier is not flipped until the grace policy decides.

Examples

Like Netflix billing. You click “subscribe” (checkout session), enter your card on Stripe’s hosted page (Polar), and Stripe pings Netflix’s back end (“payment OK”), at which point your account flips to premium. If you cancel, Netflix lets you watch until the month ends; only then does the badge disappear.

neighbors on the map