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_countcolumn, 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_eventstable andlemon_*columns onusers. - 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 tostripe_*. Stripe usesStripe-Signatureheader (timestamp + HMAC-SHA256) instead of LemonSqueezy’sX-Signature. Stripe Checkout Sessions via API replace LemonSqueezy hosted checkout URLs. Config env vars change fromGLB_LEMON_*toGLB_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_SECRETGLB_STRIPE_SECRET_KEYGLB_STRIPE_STANDARD_PRICE_IDGLB_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_idembedded in checkout metadata (server-generated, not client-supplied). Unmatched events logged withuser_id=NULLfor 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_eventstable 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.refundedevent triggersActionDowngradeToFreewith force-disconnect. addon_relay_countis a denormalized cache onusers. It becomes the source of truth when updated by webhook events. A futureuser_addonstable was planned but the column approach is working.- Stripe Checkout Sessions require
StripeSecretKeyfor 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_addonstable is still needed or ifaddon_relay_countonusersis sufficient long-term. - Admin dashboard billing management UI (link to Stripe customer portal vs. in-app management).
- Free trial implementation (deferred, no timeline).