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_settingsPlan feature limits in
billing_plansProduct catalog metadata and activation state
Organization
billing_emailandstripe_customer_idOne-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_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,readOrganization Manager/Admin/Super Admin:
code-access:manage
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 organization fallback. Subscription purchase emails use the
Better Auth Stripe checkout metadata userId first, then Stripe checkout email,
then the same organization fallback. Subscription updates use the
subscription's userId metadata, then the fallback; invoice emails use the
invoice's actor metadata, then the invoice customer email, then the fallback.
The actor receives the email only while they are still a member of the
organization and can() grants them billing:read there: an actor who left
the organization, or who lost that permission, receives nothing, and the email
goes to the next recipient in that order. The invoice customer email is the
Stripe Customer email, which is the checkout actor's account email while the
organization has no billing email of its own; save a billing contact so later
invoices do not reach someone who left the organization.
The fallback is the organization's longest-standing member for whom can()
grants billing:read in that organization, because the email discloses what
that permission shows on /dashboard/organization/billing. A Better Auth role
(owner, admin, manager) qualifies nobody by itself: when no member holds
billing:read, the email is not sent and only the organization id is logged.
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.