Skip to content

feat(billing): integrate Stripe subscriptions via Better Auth#2160

Merged
koistya merged 9 commits into
mainfrom
dev
Feb 15, 2026
Merged

feat(billing): integrate Stripe subscriptions via Better Auth#2160
koistya merged 9 commits into
mainfrom
dev

Conversation

@koistya

@koistya koistya commented Feb 15, 2026

Copy link
Copy Markdown
Member

Summary

Integrates Stripe billing using the @better-auth/stripe plugin — customer lifecycle, subscription management, and webhook handling all managed by the auth layer.

  • Add subscription schema, remove unused team table, fix invitation status type
  • Configure @better-auth/stripe plugin with plan definitions, org-level billing authorization, and optional activation (works without Stripe env vars)
  • Add billing.subscription tRPC query with TanStack Query integration (cache key includes activeOrgId for automatic refetch on org switch)
  • Add billing card to settings page with upgrade/manage flows
  • Add billing spec, infra docs for webhook output and worker secrets

How it works

Browser → auth.subscription.upgrade() → Stripe Checkout (hosted)
Stripe  → POST /api/auth/stripe/webhook → plugin updates subscription table
Browser → trpc.billing.subscription.query() → cached via TanStack Query

Mutations go through the Better Auth client (plugin handles Stripe API, session validation, org authorization). Reads go through tRPC for TanStack Query caching and batching.

Billing reference

The server derives referenceId from the session — org billing when activeOrganizationId is set, personal billing otherwise. No client-side parameter needed.

Key files

Layer Files
Schema db/schema/subscription.ts
Config apps/api/lib/plans.ts, apps/api/lib/stripe.ts
Auth Stripe plugin in apps/api/lib/auth.ts
Router apps/api/routers/billing.ts
Client apps/app/lib/queries/billing.ts
UI Billing card in apps/app/routes/(app)/settings.tsx
Spec docs/specs/billing.md

Plan limits (single source of truth in apps/api/lib/plans.ts)

export const planLimits = {
  free: { members: 1 },
  starter: { members: 5 },
  pro: { members: 50 },
} as const;

Stripe config is optional

All Stripe env vars are checked at startup — if any are missing, the plugin is not loaded. The app works normally without billing; endpoints simply don't exist.

Links

…s type

Replace invitation status pgEnum with text() to avoid fragile coupling
with Better Auth's values. Remove unused team/teamMember tables and
session.activeTeamId (teams plugin not enabled). Add subscription table
and stripeCustomerId columns for upcoming Stripe billing integration.
Move team schema to docs/recipes/teams.md as an opt-in recipe.
Wire up @better-auth/stripe for subscription management with hosted
Checkout. Server derives billing context from session (org vs personal).
tRPC provides read-only subscription query; mutations go through the
Better Auth client directly.
- Add comprehensive tests for billing router endpoints
- Create Vitest config for API tests
- Add billing queries tests for app
- Update billing spec documentation
Stripe env vars are now optional — the app starts and works without
them, billing endpoints simply return 404. This removes the setup
barrier for contributors who don't need billing features.
Replaces inline spread-ternary with a stripePlugin() helper that
encapsulates both the env guard and plugin configuration, keeping
the betterAuth plugins array flat and scannable.
The `apps/*` glob matched `apps/email`, which has no vitest config
but contains a generated `.react-email/` directory with its own
vitest config and spec files, causing "describe is not defined" errors.
@koistya koistya merged commit 40dbcde into main Feb 15, 2026
8 checks passed
@koistya koistya deleted the dev branch February 15, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant