Billing & Stripe

Summary

GoLiveBro’s billing system manages subscription lifecycle for the managed relay product. The original integration (Phase 5a, designed 2026-03-23) used LemonSqueezy as Merchant of Record. A migration to Stripe was planned on 2026-04-08 to replace LemonSqueezy entirely. The billing layer is well-abstracted: WebhookEvent and Processor.DetermineAction() are provider-agnostic, so only three layers change in the migration: webhook signature verification, event parsing, and checkout URL generation.

Pricing: Free tier (9.99/mo, 1 managed relay included), Add-on Relay (99/yr) is deferred. Internal tier (operators) gets 99 managed relays. The addon_relay_count column on users tracks purchased add-ons, updated by webhook events.

The webhook flow works as follows: the checkout URL embeds a user_id in metadata, linking the payment back to the GoLiveBro account. Webhook events (signature-verified) are stored in a billing_events audit table with idempotent dedup, then processed to update plan_tier, plan_status, and addon_relay_count on the users row. A 48-hour grace period follows payment failure before downgrade to free tier with force-disconnect of active relay sessions. A polling reconciliation job runs hourly to catch missed webhooks.

Timeline

  • 2026-03-22: Phase 5 entitlement enforcement approved. Defined free/standard/internal tiers, addon_relay_count column, server-side relay limit enforcement. Migration 0015 renames starter to free, pro to standard.
  • 2026-03-23: Phase 5a LemonSqueezy billing design approved. Approach B (webhook + polling reconciliation) selected. Two LemonSqueezy products: Standard Plan (7.99/mo). Migration 0019 adds billing_events table and lemon_* columns on users.
  • 2026-03-23: LemonSqueezy billing implementation plan written. 10 tasks covering migration, config, webhook verification (HMAC-SHA256), event parsing, store methods, billing handler, checkout URL generation, grace period job, polling reconciliation, and dock UI.
  • 2026-04-08: LemonSqueezy to Stripe migration plan written. Migration 0030 renames lemon_* columns to stripe_*. Stripe uses Stripe-Signature header (timestamp + HMAC-SHA256) instead of LemonSqueezy’s X-Signature. Stripe Checkout Sessions via API replace LemonSqueezy hosted checkout URLs. Config env vars change from GLB_LEMON_* to GLB_STRIPE_*.

Current State

The billing code is being migrated from LemonSqueezy to Stripe. The existing billing architecture is in place with the following components:

Database: billing_events table stores all webhook payloads with idempotent dedup via unique event ID. Users table has billing columns (being renamed from lemon_* to stripe_* in migration 0030): stripe_customer_id, stripe_subscription_id, stripe_addon_subscription_id, plan_grace_deadline.

Go packages: internal/billing/verify.go handles webhook signature verification. internal/billing/events.go parses webhook JSON into WebhookEvent structs. internal/billing/processor.go maps events to actions via DetermineAction(). internal/api/billing_handlers.go handles the webhook endpoint and checkout URL generation.

Webhook endpoint: POST /api/v1/billing/webhook (unauthenticated, signature-verified). Processing flow: verify signature, store raw event (idempotent skip on duplicate stripe_event_id), determine action, update user state, mark processed.

Checkout: POST /api/v1/billing/checkout (JWT-authed). Request body specifies "product": "standard" or "addon_relay". For Stripe, creates a Checkout Session via API with metadata.user_id. Dock “Upgrade” button calls this endpoint and opens result URL in browser.

Background jobs (telemy-jobs):

  • Grace period enforcement: every 5 minutes, queries users where plan_status='past_due' AND plan_grace_deadline < NOW(), downgrades to free, force-disconnects active relays.
  • Polling reconciliation: every hour, queries all users with a subscription ID, calls Stripe API to compare state, updates DB on mismatch.

Stripe env vars (not yet configured on production):

  • GLB_STRIPE_WEBHOOK_SECRET
  • GLB_STRIPE_SECRET_KEY
  • GLB_STRIPE_STANDARD_PRICE_ID
  • GLB_STRIPE_ADDON_PRICE_ID

Key Decisions

  • 2026-03-23: Approach B (webhook + polling reconciliation) selected over Approach A (webhook only). Polling catches missed webhooks and manual LemonSqueezy/Stripe dashboard changes. Hourly poll is acceptable cost for data consistency.
  • 2026-03-23: Two separate products for base plan and add-on relay. Quantity-based pricing on a single product would assume uniform unit price (7.99).
  • 2026-03-23: 48-hour grace period after payment failure before downgrade. Balance between user convenience and cost protection.
  • 2026-03-23: User matching via user_id embedded in checkout metadata (server-generated, not client-supplied). Unmatched events logged with user_id=NULL for manual review.
  • 2026-03-23: Add-on relay count capped at 3 (4 total managed). Internal tier exempt from cap.
  • 2026-04-08: Migration from LemonSqueezy to Stripe. Billing layer is provider-agnostic at the processor level, so only verification, parsing, and checkout generation change. No new Go dependencies needed; Stripe verification done with stdlib crypto/hmac.
  • 2026-04-08: Stripe uses Price IDs (price_xxx) instead of Product IDs for checkout. No checkout base URL needed because checkout sessions are created via Stripe API rather than hosted URL construction.

Gotchas & Known Issues

  • Stripe env vars (GLB_STRIPE_*) are not yet configured on production. Billing webhooks return “not configured” until set.
  • Billing features degrade gracefully if config env vars are unset. No hard validation at startup. Webhook handler and checkout endpoint check at request time.
  • Stripe webhook timestamp tolerance is 5 minutes. Events older than that are rejected as potential replay attacks.
  • billing_events table may contain PII. Same access controls as postgres.
  • No billing data is exposed to dock JS. Dock sees entitlement status only (plan tier, plan status, allowed/denied).
  • Force-disconnect on downgrade: all active relay sessions are terminated when a user’s plan expires or grace period ends.
  • Refund handling: charge.refunded event triggers ActionDowngradeToFree with force-disconnect.
  • addon_relay_count is a denormalized cache on users. It becomes the source of truth when updated by webhook events. A future user_addons table was planned but the column approach is working.
  • Stripe Checkout Sessions require StripeSecretKey for API calls (server-side only, never exposed to client).
  • Annual pricing ($99/yr) is deferred until monthly subscription model is proven.
  • The polling reconciliation job calls the Stripe API for each subscribed user. At scale, this may need batching or rate limit awareness.

Open Questions

  • Production Stripe env vars need to be configured before billing goes live.
  • Whether the polling reconciliation interval (hourly) is sufficient or should be adjusted.
  • Annual pricing timing and implementation.
  • Whether the user_addons table is still needed or if addon_relay_count on users is sufficient long-term.
  • Admin dashboard billing management UI (link to Stripe customer portal vs. in-app management).
  • Free trial implementation (deferred, no timeline).

Sources