Billing

Summary

Telemy uses LemonSqueezy (Merchant of Record, acquired by Stripe in 2024) as its billing provider. LemonSqueezy handles payment processing, tax compliance, VAT, invoices, receipts, refunds, and chargebacks. The Go control plane receives webhook events via HMAC-SHA256-signed POST /api/v1/billing/webhook and maintains a local mirror of subscription state in the users and billing_events tables for entitlement enforcement. Checkout URLs are generated server-side via the JWT-authenticated POST /api/v1/billing/checkout endpoint, which embeds user_id in LemonSqueezy custom metadata so webhook payloads can be matched back to Telemy accounts.

Pricing is structured around two tiers: Free (9.99/mo, 1 managed relay included). Add-on relays cost 99/yr) is deferred until monthly is proven. The Pro tier was intentionally deferred.

The billing system was implemented across two phases: Phase 5 (entitlement enforcement, 2026-03-22) established the free/standard/internal tier structure and server-side relay gating, and Phase 5a (LemonSqueezy integration, 2026-03-23) added webhook processing, checkout flow, grace period enforcement, and polling reconciliation. All code is deployed to the Advin server. The system is currently blocked on LemonSqueezy business store approval, pending social media URLs and a demo video from Michael.

Timeline

  • 2026-03-22: Phase 5 design approved. Tier structure finalized: free/standard/internal replacing legacy starter/pro. Migration 0015 written.
  • 2026-03-22: Entitlement enforcement implemented: GetRelayEntitlement() refactored with pure computeEntitlement() function, ErrPoolCapacityFull sentinel error, GET /api/v1/capacity/status public endpoint added.
  • 2026-03-22: Dock UI updated to disable “Add Managed” button for free-tier and at-limit users with contextual messages.
  • 2026-03-22: Migration 0015 applied to Advin. Operator accounts set to internal tier.
  • 2026-03-23: Phase 5a design approved. LemonSqueezy chosen as billing provider (Approach B: Webhook + Polling Reconciliation).
  • 2026-03-23: Billing code implemented: internal/billing/ package (verify, events, processor), store layer (store_billing.go), webhook handler, checkout endpoint, grace enforcement job, subscription reconciliation job.
  • 2026-03-23: Migration 0019 applied to Advin. billing_events table and LemonSqueezy columns on users created.
  • 2026-03-23: LemonSqueezy products created: Standard Plan (ID 915393, 7.99/mo).
  • 2026-03-27: LemonSqueezy support (Kashish) requested additional info for store approval: pricing breakdown, demo video, social URLs, product description, platform integrations.
  • 2026-03-31: Phase 5a code fully deployed. Blocked on store approval.

Current State

All billing code is deployed and running on Advin. The webhook endpoint is live at https://api.telemyapp.com/api/v1/billing/webhook. Grace enforcement runs every 5 minutes. Subscription reconciliation runs every hour (stub implementation until production API keys are active). Dock UI shows upgrade banner for free-tier users and past-due warning with countdown for users in grace period.

Blocker: LemonSqueezy business store approval is pending. Michael needs to provide social media URLs and record a demo video showing the OBS dock, relay, and telemetry features. No code changes are needed. Once approved, end-to-end testing can proceed with test card 4242 4242 4242 4242.

Environment variables configured on Advin:

  • TELEMY_LEMON_WEBHOOK_SECRET (from LemonSqueezy dashboard)
  • TELEMY_LEMON_API_KEY (from LemonSqueezy dashboard)
  • TELEMY_LEMON_STANDARD_PRODUCT_ID=915393
  • TELEMY_LEMON_ADDON_PRODUCT_ID=915398
  • TELEMY_LEMON_CHECKOUT_BASE_URL=https://telemyapp.lemonsqueezy.com/checkout/buy

Webhook URL configured in LemonSqueezy: https://api.telemyapp.com/api/v1/billing/webhook with events: subscription_created, subscription_updated, subscription_payment_success, subscription_payment_failed, subscription_cancelled, subscription_expired.

Key Decisions

  • 2026-03-22: Three-tier structure (free/standard/internal) replacing starter/pro — Free tier costs nothing (BYOR only, no server resources), Standard matches market rate ($9.99, same as IRLHosting/Belabox), internal tier for operators only.
  • 2026-03-22: No Pro tier — Deferred until multi-relay demand materializes. Add-on relays serve the same purpose at lower complexity.
  • 2026-03-22: Add-on relay cap of 3 (4 total managed) per account — More available by manual DB override. Prevents resource exhaustion during beta.
  • 2026-03-22: All relays full-capability with hidden 48 Mbps soft cap — No published bitrate tiers. Matches competitor positioning (IRLHosting: 48 Mbps, 4K/60fps).
  • 2026-03-23: LemonSqueezy as billing provider (Approach B: Webhook + Polling Reconciliation) — LemonSqueezy is Merchant of Record (handles tax/VAT/refunds). Approach B uses webhooks as primary with hourly polling reconciliation to catch missed events. Scales past $50K MRR before needing a separate billing microservice.
  • 2026-03-23: Two separate LemonSqueezy products instead of quantity-based — Base plan (7.99) are different prices, so quantity-based pricing (which assumes uniform unit price) does not work.
  • 2026-03-23: 2-day grace period for failed payments — plan_status=past_due with plan_grace_deadline=NOW()+2days. Users keep relay access during grace. After expiry, the 5-minute enforcement job downgrades to free and force-disconnects active sessions.
  • 2026-03-23: past_due treated as active for entitlement checks — The grace period enforcement job handles the cutoff, not the entitlement logic. This avoids immediate access loss on a transient payment failure.
  • 2026-03-23: Checkout URL embeds user_id server-side — The user_id in LemonSqueezy custom metadata is server-generated (from JWT), not client-supplied. Prevents users from associating payments with other accounts.
  • 2026-03-23: Annual pricing ($99/yr) deferred — Ship monthly first, add annual once retention data exists.

Experiments & Results

ExperimentStatusFindingSource
Webhook + Polling Reconciliation (Approach B)DeployedPolling reconciliation catches missed webhooks and manual LemonSqueezy dashboard changes. Stubbed pending production API keys.2026-03-23-phase5a-lemonsqueezy-billing-design.md
Pure computeEntitlement() extractionDeployedEnabled 7 unit tests without database dependency. All tier/status combinations tested in isolation.2026-03-22-phase5-entitlement-enforcement.md
Server economics (Advin KVM Standard XS)Validated40 rev / $8 cost = 80% margin.[project_pricing_structure.md memory]

Gotchas & Known Issues

  • Subscription reconciliation is stubbed. The reconcileSubscriptions job logs the count of checked subscriptions but does not yet call the LemonSqueezy API. Implementation deferred until production API keys are active and store is approved.
  • addon_relay_count is a denormalized cache. Currently updated directly by webhook events. When the user_addons table ships (future), this column becomes derived. Updates must stay idempotent.
  • Webhook event dedup key is composite. The lemon_event_id stored in billing_events is {event_name}_{subscription_id}, not the LemonSqueezy-native event ID. This means the same subscription could have separate subscription_created and subscription_updated events stored, but two identical subscription_created events for the same subscription will be deduplicated.
  • Stream tokens are 8 hex chars. All users share relay ports (5000 publish, 4000 play). SLS multiplexes by streamid. Short tokens are acceptable risk for invite-only beta but must be lengthened before public launch (security backlog).
  • Free trial not implemented. LemonSqueezy supports free trials, but Telemy does not use them. The invite-only beta serves the same purpose.
  • forceDisconnectUser calls deprovision synchronously. If a user has many active sessions, the webhook handler could be slow. Consider async dispatch for force-disconnect in production.
  • Config validation is not strict. LemonSqueezy env vars are optional at startup — billing features degrade gracefully if unset. The webhook handler and checkout endpoint check at request time and return 503.
  • pgxmock test expectations may need tuning. The UpdateUserBillingState dynamic query builder produces different parameter orderings depending on which fields are set. Mock expectations must match the exact query shape.

Open Questions

  • When will LemonSqueezy store approval complete? Blocked on Michael providing social URLs and demo video.
  • Should the reconciliation polling interval be shorter than 1 hour during initial launch to catch issues faster?
  • What is the refund policy? LemonSqueezy handles refunds as MoR, but webhook state updates for refunds are not explicitly handled yet.
  • Should the grace period be longer than 2 days? Industry standard is often 3-7 days. Current 2-day window may be aggressive.
  • When should the user_addons table ship to replace denormalized addon_relay_count?
  • What happens if a user with active relays downgrades mid-stream? Force-disconnect is implemented, but should there be a softer transition (e.g., finish current stream, block next start)?
  • When will annual pricing ($99/yr) be added?
  • Is the webhook URL on a custom domain (api.telemyapp.com) or will it move to a billing-specific subdomain?

Sources

  • 2026-03-22-phase5-entitlement-enforcement-design.md
  • 2026-03-22-phase5-entitlement-enforcement.md
  • 2026-03-23-phase5a-lemonsqueezy-billing-design.md
  • 2026-03-23-phase5a-lemonsqueezy-billing-plan.md