The boilerplate supports both B2C (Business-to-Consumer) and B2B (Business-to-Business) models with a flexible account-centric architecture.
B2C vs B2B Business Models
Choose your business model by setting the businessModel property in config/app.ts to either 'b2c' or 'b2b'. This single setting controls account creation behavior, team features visibility, the billing flow, and the sidebar navigation.
Comparison Table
| Feature | B2C Mode | B2B Mode |
|---|---|---|
| Account Type | Personal accounts only | Personal + Workspace accounts |
| Account Creation | Personal account auto-created on signup (1 user = 1 account) | Personal account + workspace both auto-bootstrapped at signup via ensureWorkspaceForUser (idempotent); invitations bring teammates in afterward |
| Team Members | Single user per account | Multiple users per workspace |
| Billing | Per-user billing | Per-workspace billing (shared credits) |
| Roles | System roles (owner / admin / member) still seeded; only multi-member team management is unused | Owner, Admin, Member, Custom roles — actively used for team management |
| Invitations | Not applicable | Email invitations with role assignment |
| Account Switcher | Hidden | Visible (switch between workspaces) |
| Use Case | Individual users, solo apps | Teams, agencies, enterprises |
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ APP USER (private.app_users) │
│ resolved from provider + issuer + provider subject │
└─────────────────────────────────────────────────────────────────┘
│
│ can have multiple
▼
┌─────────────────────────────────────────────────────────────────┐
│ MEMBERSHIPS (user_id, account_id, role_slug) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ User A │ │ User A │ │ User B │ │
│ │ Account 1 │ │ Account 2 │ │ Account 2 │ │
│ │ role: owner │ │ role: member│ │ role: admin │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ belongs to
▼
┌─────────────────────────────────────────────────────────────────┐
│ ACCOUNT (personal | workspace) │
│ │
│ ├── credits_balance (shared pool for all members) │
│ ├── subscription (provider-scoped subscription) │
│ ├── chat_sessions (AI conversations) │
│ ├── ai_requests (usage logs) │
│ ├── api_keys (B2B API access) │
│ └── settings (account preferences) │
└─────────────────────────────────────────────────────────────────┘B2C Mode Details
In B2C mode, each verified user gets a personal Account through the explicit server-side application-identity bootstrap. The personal Account holds the user's subscription, credits, and chat history. There are no workspaces, invitations, or team features. The account switcher is hidden in the UI, and billing is tied to the individual user.
B2B Mode Details
In B2B mode, users can create workspaces and invite team members. Each workspace is a separate account with its own subscription, credits, and data. Users can belong to multiple workspaces and switch between them using the account switcher. Workspace owners can invite members via email, assign roles (owner, admin, member, or custom), and manage billing centrally for the entire team.
ensureWorkspaceForUser() (idempotent) at five sites — the magic-link callback,
the OAuth callback, POST /api/auth/verify-otp, the /checkout Server Component, and the free-plan path through
/api/billing/subscribe-free (via subscribeToFreePlan in core/billing/free-plan.ts).
/onboarding/workspace remains as a legacy manual-creation fallback only. The created
membership writes both role and role_slug as 'owner', so the
Manage organization entry and all permission helpers resolve immediately.
/org-dashboard: in B2B mode, the personal
/private-dashboard hides the upgrade button (sidebar, subscription card, Quick Actions). Workspace billing
lives on /org-dashboard/billing via OrgBillingActions. The signal flows from
the provider-neutral subscriptions.provider field through
checkAccountAccess's details.isProviderManaged signal combined with
appConfig.businessModel.
Organization Dashboard
The Organization Dashboard (/org-dashboard) is the central management interface for workspace owners and admins. It provides tools for managing members, billing, settings, and analytics.
owner or admin roles can access the org-dashboard. Members are automatically redirected.
Dashboard Structure
org-dashboard/ ├── admin/ # Workspace admin overview ├── members/ # Team member management │ ├── [memberId]/ # Individual member details │ └── roles/ # Role overview & permissions ├── billing/ # Subscription & credits ├── api-keys/ # B2B API keys ├── settings/ # Workspace configuration └── analytics/ # Usage analytics
Members Management
The Members page displays all workspace members with their roles and AI usage statistics.
Billing Management
The Billing page shows the current subscription, credits balance, and payment history.
┌─────────────────────────────────────────────────────────────────┐ │ BILLING DASHBOARD │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ Current Plan │ │ Credits Balance │ │ │ │ ──────────── │ │ ────────────── │ │ │ │ Pro Plan │ │ 2,498 credits │ │ │ │ $29/month │ │ ─────────── │ │ │ │ [Manage] │ │ [Buy More] │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ Credit Transactions │ │ ───────────────────────────────────────────────── │ │ │ Date │ Type │ Amount │ Balance │ │ │ │ 2024-01-15 │ AI Request │ -1 │ 2,498 │ │ │ │ 2024-01-15 │ AI Request │ -1 │ 2,499 │ │ │ │ 2024-01-01 │ Subscription│ +2,500 │ 2,500 │ │ │ ───────────────────────────────────────────────── │ │ │ │ Recent Payments │ │ ───────────────────────────────────────────────── │ │ │ Date │ Description │ Amount │ Status │ │ │ │ 2024-01-01 │ Pro Plan │ $29.00 │ Paid │ │ │ │ 2023-12-15 │ Credit Pack │ $19.00 │ Paid │ │ │ ───────────────────────────────────────────────── │ │ │ └─────────────────────────────────────────────────────────────────┘
The Billing page in the org dashboard displays the current subscription plan, credit balance, recent payments, and a link to the configured provider's customer portal for managing payment methods and invoices. Admins and owners can access this page; regular members are restricted based on role permissions.
Workspace Settings
The Settings page allows workspace configuration and includes a danger zone for destructive actions.
Settings Page Structure: ───────────────────────── 1. General Settings ├── Workspace Name (editable) ├── Workspace Slug (URL identifier) └── Created Date (read-only) 2. Organization Info ├── Account ID ├── Account Type (workspace) └── Members Count 3. Danger Zone (owner only) ├── Schedule Deletion (30-day grace period) ├── Cancel Scheduled Deletion └── Warning: Notifies all members via email
Access Control Implementation
Permission checks are performed using the checkPermission(accountId, permission) function from lib/permissions/check.ts. This function verifies the current user's membership and role in the specified account, then checks whether that role has the required permission. UI components conditionally render based on the user's role (e.g., the billing tab is only shown to owners and admins in B2C/hybrid, or owners only in B2B).
Billing-Manager Role (B2B Owner-Only)
Every billing route checks hasRole(membership, getBillingManagerRoles()) from lib/permissions/check.ts — a single source of truth for who can change financial state. The helper returns:
['owner']whenappConfig.businessModel === 'b2b'— the workspace owner is the sole accountable billing party. Admins can run the org but cannot trigger checkout, change plans, open the configured provider's customer portal, or end a trial.['owner', 'admin']in B2C / hybrid mode — admins keep their billing capability.
Routes wired to this helper: /api/billing/checkout, /api/billing/license-checkout, /api/billing/portal, /api/billing/change-plan, /api/billing/subscription-control, and /api/billing/end-trial. Portal, plan-change, lifecycle and trial mutations retain strict rate limiting and billing step-up; the read-only end-trial status uses the standard limit without another freshness challenge. These management routes pass the wrapper's active appUserId to actor-scoped Prisma helpers, while session and MFA checks retain the provider coordinate. Account membership and billing-manager checks apply in the same transaction context. /api/billing/subscribe-free stays owner-only across all modes, and /api/org/schedule-deletion remains owner-only.
Member checkout and licence checkout pass a minimal trusted actor, {'{ appUserId, email }'}, rather than an Auth SDK user. The application identity scopes memberships, deferred referral/affiliate attribution, durable attempts and signed payment context. The email comes from the revalidated principal, never request data. The common SQL and Prisma contracts use the same identity model for all four Database/Auth compositions.
When adding a new billing route, never inline hasRole(membership, ['owner', 'admin']) — import getBillingManagerRoles() so future B2B-only tightening propagates automatically.
Team Invitations
In B2B mode, workspace owners and admins invite teammates by email; each invite creates a single-use token with a 7-day expiry, then drops a membership when accepted. The full flow (token lifecycle, accept/resend API, email template builder, and security notes) lives in Team Invitations.
B2B Subscription Flow (auto-bootstrap workspace)
B2B requires the subscription, credits, invitations, and team data to attach to a workspace account. The app now creates that workspace automatically during the auth/checkout bootstrap when the user does not already have one. The selected plan is carried across the auth round-trip by the pending-checkout cookie.
- Plan selection — User clicks a plan on
/pricing. A short-lived httpOnlybsk_pending_checkoutcookie (30min, Zod-validated payload) carries the choice across the auth round-trip. sessionStorage mirrors the cookie as a same-tab fast path. - Sign up / log in — Magic link, OTP code, or OAuth. After provider verification, the callback invokes the canonical application bootstrap, which atomically creates or revalidates the profile, personal Account and owner membership before activation.
- Workspace bootstrap — In B2B mode,
ensureWorkspaceForUser()creates a workspace account and owner membership when the user has none./onboarding/workspaceremains a fallback if bootstrap fails. - Checkout —
/checkoutreads the pending checkout cookie. The client POSTs only internal purchase facts and the workspace Account target to/api/billing/checkout. The server resolvesPAYMENTS_PROVIDERand its catalogue binding, and returns400 WORKSPACE_REQUIREDif a B2B subscription targets a personal Account. The cookie is cleared before the configured-provider redirect. - Configured-provider payment — the user completes payment on Stripe, Lemon Squeezy, or Paddle according to the installation; the UI never offers that choice.
- Webhook processing — the signed provider event is journaled, normalized, and applied to the provider-neutral subscription contract, superseding the current
internalfree row on the same Account. Paid credits are granted only from normalized provider payment proof, throughadd_credits, with a semantic key scoped to the provider subscription and billing period. - Success page — the provider returns through the local status exchange. An opaque bearer is rotated into a short HttpOnly cookie before the clean
/checkout/successURL renders. Only a durablecompletedattempt may route to onboarding or the correct dashboard; no provider session ID appears in the browser URL or grants access. - Profile onboarding (when needed) —
OnboardingFormcollects first/last name and optional phone, then routes to the appropriate dashboard. Workspace renaming can be done later from/org-dashboard/settings.
The decision "send the buyer to /onboarding or directly to a dashboard?" lives in exactly one place: app/[locale]/(auth)/checkout/success/page.tsx (Server Component). Re-introducing the gate in middleware, in a Client Component, or in another Route Handler is a regression (anti-pattern A13). The middleware's onboarding gate explicitly exempts /checkout and /checkout/success, and the /login gate honors redirectTo=/checkout* before falling through to the onboarding redirect.
The workspace requirement is handled by idempotent server-side bootstrap plus API enforcement, so a B2B subscription never attaches to a personal account by accident:
callback/route.tsbootstraps after magic-link verification.callback/route.tsbootstraps after OAuth session exchange.POST /api/auth/verify-otpbootstraps after OTP code verification./checkout/page.tsxbootstraps as defense-in-depth before the checkout client runs.POST /api/billing/subscribe-freebootstraps as defense-in-depth for free plans.
POST /api/billing/checkout still returns 400 WORKSPACE_REQUIRED if a B2B subscription request targets a personal account.
This keeps the rule simple for new developers: B2C bills the personal account; B2B bills the workspace account.
Invited User Flow
When an invited user accepts an invitation:
- Click invitation link → Accept invitation page
- API-based acceptance via
/api/invitations/accept - Redirect to success page → Onboarding (if needed)
- Subscription check: If workspace has active subscription, redirect via the workspace-manager rule (
isWorkspaceManager(user.id)) —/org-dashboardfor owners/admins, else/private-dashboard(not pricing) - Dashboard prioritizes workspace account for data display
Organization Member Limits
Organizations can have configurable member limits:
| Feature | Description |
|---|---|
max_members |
Configurable per organization (null = unlimited) |
| Enforcement | Limit checked on member invitation |
| Admin Update | Can be updated via admin dashboard |
| Visual Indicator | Warning shown when limit is reached |
Roles & Permissions
The boilerplate ships a dynamic, database-driven roles system with three protected system roles (owner, admin, member), a 12-permission matrix, and custom roles managed via the admin dashboard. The full system-roles table, permission matrix, available permissions list, checkPermission() usage, and custom-roles API surface live in Roles & Permissions.