YYayaw

Billing Overview

Billing architecture and runtime model for subscriptions and one-time payments.

Overview

Yayaw billing uses a hybrid architecture:

  1. Better Auth Stripe plugin for recurring subscriptions.
  2. Custom Stripe webhook flow for one-time payments (lifetime entitlement).
  3. Runtime config split between environment values and database-managed product metadata.

Source of Truth

  • Environment variables:
    • Stripe secrets
    • Stripe price_id values
  • 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_email and stripe_customer_id
    • One-time entitlements, durable code-access grants, and webhook idempotence events

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_monthly
    • pro_yearly
    • business_monthly
    • business_yearly
  • One-time product:
    • pro_lifetime

Runtime Priority Rules

  1. If the organization has an active pro_lifetime entitlement, billing state is considered active and effective plan is pro.
  2. Otherwise, billing state follows Stripe subscription status and grace-period rules.
  3. 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:

  1. /dashboard/organization/billing (active plan summary + status/seats + activity)
  2. /dashboard/organization/plans (plan selection and checkout actions)
  3. /dashboard/organization/code-access (purchased source-code deliverables)

Both surfaces are driven by a dedicated server view-model:

  • getOrganizationBillingPageModel(organizationId)
  • overview.activeOffer is 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:

  1. Route access requires code-access:read through the existing RBAC engine.
  2. The seed and migration grant that resource through organization roles:
    • Organization Member: code-access:list,read
    • Organization Manager/Admin/Super Admin: code-access:manage
  3. Stripe one-time checkout creates or updates a billing_code_access_grants row keyed by checkout session and also grants the pro_lifetime entitlement.
  4. Better Auth Stripe subscription callbacks create, refresh, expire, or revoke the organization grant keyed by Stripe subscription ID.
  5. The page unlocks when the user has code-access:read and 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_URL
  • BILLING_CODE_ACCESS_DOWNLOAD_URL
  • BILLING_CODE_ACCESS_DOCUMENTATION_URL
  • BILLING_CODE_ACCESS_SUPPORT_URL

Configure GitHub secrets with:

  • BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY
  • BILLING_CODE_ACCESS_GITHUB_TOKEN (optional local/staging fallback)

Optional non-secret GitHub fallbacks:

  • BILLING_CODE_ACCESS_GITHUB_REPOSITORY
  • BILLING_CODE_ACCESS_GITHUB_APP_ID
  • BILLING_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 emits invoice.paid, once a paid invoice can be linked from Yayaw email.

Billing templates declare detailed purchase variables, including:

  • billing.productName, billing.planName, billing.productType
  • billing.amountFormatted, billing.currency, billing.billingInterval
  • billing.optionsSummary, billing.lineItemsSummary, billing.discountSummary
  • billing.status, billing.periodStart, billing.periodEnd
  • billing.checkoutSessionId, billing.stripeSubscriptionId, billing.stripePaymentIntentId
  • billing.invoiceId, billing.invoiceNumber, billing.invoiceUrl, billing.invoicePdfUrl, billing.billingReason
  • billing.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:

  1. Current subscription snapshot
  2. Organization entitlements (billing_entitlements)
  3. Code-access grants (billing_code_access_grants)
  4. GitHub repository access records (code_access_github_accounts)
  5. One-time webhook events (billing_webhook_events, provider stripe-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 customer to 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 pull access.
  • Missing Stripe price_id values mark products as non-checkoutable instead of crashing runtime.