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
┌─────────────────────────────────────────────────────────────────┐
│ USER (auth.users) │
│ email, created_at, etc. │
└─────────────────────────────────────────────────────────────────┘
│
│ 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 (Stripe subscription) │
│ ├── chat_sessions (AI conversations) │
│ ├── ai_requests (usage logs) │
│ ├── api_keys (B2B API access) │
│ └── settings (account preferences) │
└─────────────────────────────────────────────────────────────────┘B2C Mode Details
In B2C mode, each user gets their own personal account automatically created on signup via a database trigger. 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
subscriptions.stripe_subscription_id through checkAccountAccess's new
details.stripeSubscriptionId field 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 Stripe 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 Stripe 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/end-trial. /api/billing/subscribe-free stays owner-only across all modes (free-plan creation is identity-shaping). /api/org/schedule-deletion is owner-only at the API regardless of mode.
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. The
handle_new_user_accounttrigger onauth.usersINSERT (signup) creates the personal account. - 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 to/api/billing/checkoutwith the workspace account target. The route validates the Stripe price againstpricingConfigand returns400 WORKSPACE_REQUIREDif a B2B subscription request targets a personal account. The cookie is cleared via Server Action right before the Stripe redirect. - Stripe payment — user completes payment on Stripe.
- Webhook processing —
customer.subscription.createdupserts the row, cancels any legacy free row (stripe_subscription_id IS NULL) on the same account, and grants the plan's full credits via theadd_creditsRPC. Idempotent via UNIQUEpayments.stripe_session_id = sub_init_${sub.id}. - Success page — Stripe redirects to
/checkout/success?session_id=cs_…. The Server Component readsprofile.onboarding_completed+ runsisWorkspaceManagerin parallel: if the user hasn't yet onboarded,dashboardPath = /onboarding; otherwise dashboard is chosen by the workspace-manager rule (/org-dashboardfor owners/admins, else/private-dashboard). - 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.