Billing Overview
Billing architecture and runtime model for subscriptions and one-time payments.
Overview
Yayaw billing uses a hybrid architecture:
- Better Auth Stripe plugin for recurring subscriptions.
- Custom Stripe webhook flow for one-time payments (lifetime entitlement).
- Runtime config split between environment values and database-managed product metadata.
Source of Truth
- Environment variables:
- Stripe secrets
- Stripe
price_idvalues
- Database (
system_settings+ billing product tables):- Grace period in
system_settings - Plan feature limits in
billing_plans - Product catalog metadata and activation state
- Organization
billing_emailandstripe_customer_id - One-time entitlements, durable code-access grants, and webhook idempotence events
- Grace period in
Yayaw is the source of truth for billing contact email. Stripe Customer email is synchronized from the organization billing email, falling back to the checkout actor's account email when the organization has no explicit billing email. Stripe customer details are never used to change the user's login email.
Current V1 Product Model
- Subscription products:
pro_monthlypro_yearlybusiness_monthlybusiness_yearly
- One-time product:
pro_lifetime
Runtime Priority Rules
- If the organization has an active
pro_lifetimeentitlement, billing state is considered active and effective plan ispro. - Otherwise, billing state follows Stripe subscription status and grace-period rules.
- Seat enforcement still applies using the effective plan seat limit.
Organization Billing UX Model (V2.3)
The organization billing UX is split into three dashboard surfaces:
/dashboard/organization/billing(active plan summary + status/seats + activity)/dashboard/organization/plans(plan selection and checkout actions)/dashboard/organization/code-access(purchased source-code deliverables)
Both surfaces are driven by a dedicated server view-model:
getOrganizationBillingPageModel(organizationId)overview.activeOfferis resolved server-side (free,pro_subscription,business_subscription,pro_lifetime)- product CTA states are resolved server-side (
enabled/disabled + reason) - product cards include Stripe price mirrors, billing intervals, plan limits, marketing features, and active-offer highlighting
- checkout client executes only action kinds declared in the view-model (
subscription_checkout,one_time_checkout,billing_portal) - the Stripe billing portal action stays disabled until the organization has a recorded Stripe customer, which avoids exposing a portal action before the first successful checkout
- organization settings expose a billing contact card; saving it updates the stored billing email and synchronizes any existing Stripe Customer
- checkout requests pass the active locale through to Stripe so Checkout and follow-up billing emails can use the right locale context
This prevents business-rule drift between server and client.
Purchased Code Access
Purchased code access is derived server-side from both authorization and billing state, with durable grants persisted when Stripe confirms a purchase:
- Route access requires
code-access:readthrough the existing RBAC engine. - The seed and migration grant that resource through organization roles:
- Organization Member:
code-access:list,read - Organization Manager/Admin/Super Admin:
code-access:manage
- Organization Member:
- Stripe one-time checkout creates or updates a
billing_code_access_grantsrow keyed by checkout session and also grants thepro_lifetimeentitlement. - Better Auth Stripe subscription callbacks create, refresh, expire, or revoke the organization grant keyed by Stripe subscription ID.
- The page unlocks when the user has
code-access:readand either an active paid billing state or an active durable grant. Grace-period billing still unlocks for non-free plans, while inactive, canceled, restricted, expired, or revoked states keep deliverables locked.
Code-access grants are organization-scoped purchase records, not per-user permissions. User access remains controlled by the authz resource and organization group roles.
GitHub repository access is provisioned separately through
code_access_github_accounts. A signed-in user with code-access:read and an
active organization grant can submit a GitHub username from
/dashboard/organization/code-access. Yayaw then invites that account to the
configured private repository with read-only pull access. GitHub App
credentials are preferred for production provisioning; a token fallback exists
for local or staging environments.
Non-secret GitHub repository access settings are managed from
/dashboard/admin/billing-settings:
- enabled/disabled
- repository full name
- GitHub App ID
- GitHub App installation ID
Environment variables remain available as fallbacks for these non-secret values, but GitHub secrets always stay in environment variables. See Deployment Environment Setup for the complete GitHub App setup and environment-variable retrieval workflow.
The grant workflow is intentionally not exposed through MCP yet because grants are derived from billing events rather than operator-managed records. GitHub repository invites now have operational state and should be exposed through control-plane tools when retry/revoke/reconcile workflows become operator actions.
Configure deliverable URLs with:
BILLING_CODE_ACCESS_REPOSITORY_URLBILLING_CODE_ACCESS_DOWNLOAD_URLBILLING_CODE_ACCESS_DOCUMENTATION_URLBILLING_CODE_ACCESS_SUPPORT_URL
Configure GitHub secrets with:
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEYBILLING_CODE_ACCESS_GITHUB_TOKEN(optional local/staging fallback)
Optional non-secret GitHub fallbacks:
BILLING_CODE_ACCESS_GITHUB_REPOSITORYBILLING_CODE_ACCESS_GITHUB_APP_IDBILLING_CODE_ACCESS_GITHUB_APP_INSTALLATION_ID
Checkout success redirects to /dashboard/organization/code-access so the
customer lands on the access surface after purchase confirmation.
Billing Transactional Emails
Billing webhooks send transactional emails through database-managed system
templates when RESEND_API_KEY and active templates are available. Missing
email configuration or missing templates are logged and do not fail webhook
processing.
Built-in billing template slugs:
billing-purchase-confirmation: sent after successful subscription checkout and after successful one-time lifetime checkout.billing-subscription-updated: sent when a subscription cancellation is scheduled or when Stripe confirms subscription deletion.billing-invoice-available: sent after Stripe emitsinvoice.paid, once a paid invoice can be linked from Yayaw email.
Billing templates declare detailed purchase variables, including:
billing.productName,billing.planName,billing.productTypebilling.amountFormatted,billing.currency,billing.billingIntervalbilling.optionsSummary,billing.lineItemsSummary,billing.discountSummarybilling.status,billing.periodStart,billing.periodEndbilling.checkoutSessionId,billing.stripeSubscriptionId,billing.stripePaymentIntentIdbilling.invoiceId,billing.invoiceNumber,billing.invoiceUrl,billing.invoicePdfUrl,billing.billingReasonbilling.manageUrl,billing.accessUrl,billing.deliverablesSummary
One-time purchases send to the checkout actor first, then the Stripe checkout
email, then the first owner/admin/manager fallback for the organization.
Subscription purchase emails use the Better Auth Stripe checkout metadata
userId first, then Stripe checkout email, then the same organization fallback.
Invoice emails resolve the organization from invoice metadata first,
subscription metadata second, and local subscription/customer rows last.
One-time Checkout enables Stripe invoice creation and stores organization,
actor, product, plan, and locale metadata on generated invoices. If Stripe
native receipt or paid-invoice emails are enabled, customers may receive both
Stripe and Yayaw email for the same invoice.
Activity Timeline Scope
The timeline intentionally exposes internal billing activity, not a full Stripe invoice ledger:
- Current subscription snapshot
- Organization entitlements (
billing_entitlements) - Code-access grants (
billing_code_access_grants) - GitHub repository access records (
code_access_github_accounts) - One-time webhook events (
billing_webhook_events, providerstripe-one-time)
One-time webhook items are filtered by organizationId from persisted Stripe payload metadata.
Safety Rules
- One-time checkout is blocked when an organization already has a non-canceled active Stripe-backed subscription with a recorded Stripe subscription ID.
- One-time checkout creates or reuses the organization Stripe Customer and
passes
customerto Stripe Checkout. When the Customer has a valid email, Checkout pre-fills and locks the email field. - One-time webhook processing is idempotent via persisted event IDs.
- Code-access grant upserts are idempotent by Stripe checkout session or Stripe subscription ID.
- GitHub repository access requests are idempotent by organization, user, and
configured repository, and only grant read-only
pullaccess. - Missing Stripe
price_idvalues mark products as non-checkoutable instead of crashing runtime.