Skip to content

systems billing and subscriptions

Claude edited this page Sep 23, 2026 · 1 revision

Billing and subscriptions

Active contributors: ferdiiskandar

Purpose

Billing decides what an authenticated user may do: which daily quota applies, which models are unlocked, and whether a paid tier is active. It creates a Xendit invoice for upgrades, records the payment, and activates the subscription when the gateway webhook confirms payment. Free usage is governed by the same code, so a guest or an unpaid user runs on the GRATIS tier.

Directory layout


  lib/billing/plans.ts                  tier limits, pricing, labels
  lib/billing/guard.ts                  quota checks and usage tracking
  lib/billing/subscription-service.ts   invoice, activation, expiry
  lib/billing/xendit-client.ts          Xendit HTTP calls and webhook token check
  desktop/ipc/subscription.ts           subscription:upgrade handler

Key abstractions

Symbol Kind Path
TIER_LIMITS, TIER_PRICING, TIER_LABELS, getLimit, isModelAvailable tier definitions lib/billing/plans.ts
checkUsageQuota, checkAndTrackUsage, trackUsage quota functions lib/billing/guard.ts
checkModelAccess, getUserTier, getUsageSummary access and summary lib/billing/guard.ts
createSubscriptionInvoice, activateSubscription, handlePaymentFailed, cancelSubscription, processExpiredSubscriptions lifecycle service lib/billing/subscription-service.ts
verifyWebhookToken, createInvoice, getInvoice gateway client lib/billing/xendit-client.ts
handleSubscriptionCommand desktop IPC handler desktop/ipc/subscription.ts

Four tiers exist. GRATIS allows 20 transforms, 3 optimizations, 3 evaluations, and 3 recommends per day, no cloud history, and only openai-gpt4o and claude-sonnet. PRO lifts transforms to unlimited with 50/30/20 for the LLM tools. TIM raises those to 200/100/50 and allows 10 team members. ENTERPRISE is unlimited across the board with API access and 100 team members. A limit of -1 means unlimited. Pricing is PRO at 49,000 IDR monthly or 399,000 yearly, TIM at 149,000 or 1,249,000, and ENTERPRISE on custom pricing; trial lengths are 7, 14, and 30 days.

getUserTier reads the Subscription row and returns GRATIS unless the status is ACTIVE. checkUsageQuota computes daily usage from UsageRecord keyed by (userId, type, date). checkAndTrackUsage does the check and increment in one atomic upsert, so the count is read after increment and allowed is count <= limit; unlimited tiers still call trackUsage for analytics. checkModelAccess delegates to isModelAvailable, which honors the ["*"] wildcard.

How it works

graph TD
    Renderer[Desktop renderer] -->|subscription:upgrade| IPC[desktop/ipc/subscription.ts]
    IPC --> Invoice[createSubscriptionInvoice]
    Invoice --> XenditCreate[xendit-client createInvoice]
    Invoice --> PaymentPending[Payment row PENDING]
    Xendit[("Xendit webhook")] --> Activate[activateSubscription]
    Activate --> PaymentPaid[Payment row PAID]
    Activate --> SubActive[Subscription ACTIVE with period]
    Activate --> Admin[createSupabaseAdminClient app_metadata tier]
    Guard[guard.ts] --> UsageRecord[(usage_records)]
    Guard --> Subscription[(subscriptions)]
Loading

handleSubscriptionCommand requires tier and interval, then calls createSubscriptionInvoice. That function computes the amount from TIER_PRICING, builds an invoice number CTE-<TIER>-<timestamp>, calls Xendit with IDR currency, a 24-hour duration, and a fixed payment-method list, and inserts a PENDING Payment whose gatewayResponse records requestedTier and requestedInterval for later webhook lookup. The redirect URLs come from NEXT_PUBLIC_APP_URL.

activateSubscription looks the payment up by gatewayPaymentId, returns early if already PAID, reads the requested tier and interval from gatewayResponse, maps the Xendit channel to a PaymentMethod, sets the payment to PAID, and updates the subscription to ACTIVE with a monthly or yearly period. It then writes the tier into Supabase app_metadata through the service-role client so JWT-based checks agree with Prisma.

handlePaymentFailed marks a payment FAILED or EXPIRED. cancelSubscription sets the subscription to CANCELED and records canceledAt; the tier stays until period end. processExpiredSubscriptions is the cron entry point: it finds CANCELED or PAST_DUE subscriptions past currentPeriodEnd, sets them to GRATIS and EXPIRED, and syncs the Supabase metadata. verifyWebhookToken compares the callback token with timingSafeEqual, and createInvoice/getInvoice authenticate with a Basic header built from XENDIT_SECRET_KEY.

Integration points

The Optimizer and Evaluator IPC paths call getUserTier and checkAndTrackUsage before running a paid tool, and call checkModelAccess when a model is selected. Region-free read-only Transform does not. See Optimizer and Evaluator. Payment and quota rows are defined in Data and Prisma, required environment variables in Configuration, and secret handling in Security.

Entry points for modification

  • Change limits, pricing, or model lists: lib/billing/plans.ts.
  • Change quota or tracking behavior: lib/billing/guard.ts.
  • Change invoice fields or payment-method mapping: lib/billing/subscription-service.ts and lib/billing/xendit-client.ts.
  • Change the desktop upgrade payload: desktop/ipc/subscription.ts.

Key source files

File What it holds
lib/billing/plans.ts Tier limits, pricing, labels, model access
lib/billing/guard.ts Quota checks, atomic usage tracking, tier lookup
lib/billing/subscription-service.ts Invoice creation, activation, cancellation, expiry
lib/billing/xendit-client.ts Xendit HTTP client and webhook token verification
desktop/ipc/subscription.ts subscription:upgrade command handler

Clone this wiki locally