@buildbase/sdk
v0.0.60
Published
A SDK for Buildbase
Maintainers
Readme
@buildbase/sdk
A React SDK for BuildBase that provides essential components to build SaaS applications faster. Skip the plumbing and focus on your core product with built-in authentication, workspace management, billing, and more.
Also works server-side (Next.js API routes, Express, Hono) — see Server-Side Usage below.
📑 Table of Contents
- Features
- Customization Map
- Installation
- Quick Start
- UI Configuration
- Authentication
- Redirect Preservation
- Affiliate / Referral Tracking
- Role-Based Access Control
- Feature Flags
- Subscription Gates
- Trial Gates
- Push Notifications
- Notifications
- Connected Agents
- Devices & Sessions
- User Management
- Workspace Management
- Public Pricing (No Login)
- Multi-Currency & Pricing Utilities
- Quota Usage Tracking
- Quota Gates
- Credit System
- Credit Gates
- Beta Form Component
- Event System
- Error Handling
- Settings
- Configuration Reference
- Common Patterns
- Troubleshooting
- API Reference
- Best Practices
- Server-Side Usage
- Webhook Verification
- Agent Readiness (Discovery)
- MCP Server
- OAuth2 App Bridge
🚀 Features
- 🔐 Authentication System - Complete auth flow with sign-in/sign-out and redirect preservation
- 🏢 Workspace Management - Multi-workspace support with switching capabilities
- 👥 Role-Based Access Control - User roles and workspace-specific permissions
- 🎯 Feature Flags - Workspace-level and user-level feature toggles
- 📋 Subscription Gates - Show or hide UI based on current workspace subscription (plan)
- ⏳ Trial Gates -
WhenTrialing,WhenNotTrialing,WhenTrialEndingcomponents +useTrialStatushook - 🔔 Push Notifications - Browser push notifications with
usePushNotificationshook, auto-triggers for billing events, and campaign management - 📬 Notifications - Email + push notification system with per-event channel control, workspace preferences, and server-side
notification.send()API - 🔌 Connected Agents -
<ConnectedAgents />screen to list/revoke authorized AI agents, plus a built-in "Connect an agent" setup guide (ChatGPT, Claude, Cursor, VS Code, Windsurf, Cline) driven by anmcpprovider prop - 📱 Devices & Sessions -
<Devices />and<Sessions />screens (Browser · OS, location + IP) to rename / sign out / remove devices and revoke sessions; server-sidebb.devices/bb.sessions, per-action show/hide + label overrides - 💺 Seat-Based Pricing - Per-seat billing with included seats, billable seat tracking, and seat limit enforcement
- 💱 Multi-Currency - Per-currency pricing variants with workspace billing currency lock
- 🤝 Affiliate Tracking - Pass referral data to Stripe checkout via
getCheckoutStripeParamsprop (Rewardful, Endorsely, FirstPromoter, etc.) - 📊 Quota Usage Tracking - Record and monitor metered usage (API calls, storage, etc.) with real-time status
- 📈 Usage Dashboard - Built-in workspace settings page showing quota consumption, overage billing breakdowns, and billing period info
- 👤 User Management - User attributes and feature flags management
- 📝 Beta Form - Pre-built signup/waitlist form component
- 📡 Event System - Subscribe to user and workspace events
- 🛡️ Error Handling - Centralized error handling with error boundaries
- 💳 Credit System - Prepaid credit balances with consume, credit gate components, and built-in workspace settings for purchasing and transaction history
- 🖥️ Server-Side SDK -
BuildBase()factory for API routes, background jobs, Express, Hono — zero React dependency - 🌐 Internationalization (i18n) - 8 locales (en, es, fr, de, ja, zh, hi, ar), ICU MessageFormat, RTL support, native numerals
- 🏠 Workspace Modes - Personal (solo B2C) or Platform (multi-user B2B), configured from admin dashboard
🎨 Customization Map
Everything the SDK lets you configure, override, or extend — one row per knob, so you can see the whole surface before reading any deep-dive section. The philosophy throughout: safe defaults, everything overridable, the SDK accelerates but never gates.
| What you customize | How | Where to read more |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Provider setup & workspace behavior | <SaaSOSProvider> / <BuildBaseProvider> props (serverUrl, orgId, locale, workspace auto-create, redirects) | Configuration Reference |
| Which SDK UI renders at all | ui prop: hide/show settings sections, workspace switcher, per-screen toggles | UI Configuration · docs/UI-CONFIG.md |
| Every user-facing string | ui.messages deep-override (e.g. rename "Subscription" → "Billing"), per-locale | UI Configuration |
| Language & number systems | locale prop — 8 locales, ICU plurals, RTL (ar), native numerals | Internationalization |
| Look & feel | CSS custom properties (--primary, --destructive, …) — semantic tokens, light/dark, no palette lock-in | docs/THEMING.md |
| Date/number formats | ui.formats (Intl options passed through) | docs/UI-CONFIG.md |
| Conditional UI | Gate components: WhenAuthenticated, WhenPermission, WhenWorkspaceFeatureEnabled, WhenSubscription, WhenTrialing, WhenQuotaAvailable, WhenCreditsAvailable, … | Subscription Gates & siblings |
| Checkout behavior | getCheckoutStripeParams (affiliate/referral params), plan-picker behavior config | Affiliate Tracking |
| Connect-an-agent guide | mcp prop (url, name, docsUrl, clients, prompt) → in-app setup guide + <ConnectedAgents /> screen | Connected Agents |
| Devices & sessions screens | <Devices /> / <Sessions /> props (showRename/showSignOut/showRemove, *Label) or ui.settings.devices toggles | Devices & Sessions |
| Error presentation | ui.errorBoundary, centralized handleError reporting | Error Handling |
| React-free server access | BuildBase() factory — workspaces, usage, credits, notifications from API routes/jobs | Server-Side Usage |
| Agent discovery surface | createAgentStack / resolveAgentPath: robots.txt, llms.txt, .well-known/*, all documents overridable (e.g. config.llmsTxt) | Agent Readiness · docs/MCP-AND-AGENT-READINESS.md |
| MCP tools | builtinTools ('readonly'/'all'/false/include/exclude) + your own via defineMcpTool (custom tools override built-ins) | MCP Server |
| MCP resources & prompts | Opt-in builtinResources catalog + defineMcpResource/defineMcpResourceTemplate/defineMcpPrompt | MCP Server |
| MCP auth & hardening | auth.verify (your token format) or buildbaseAuth preset; rateLimit, cors, allowedOrigins, formatToolError | docs/MCP-AND-AGENT-READINESS.md |
| Per-request context for your MCP tools | context(auth, req) factory → ctx.custom (your DB, services) | MCP Server |
| Money display | formatMinorAmountIntl/formatCents/getCurrencyDecimals — full ISO 4217 (0/2/3-decimal currencies) | Multi-Currency |
| Webhooks from the platform | verifyWebhookSignature (HMAC, timing-safe) | Webhook Verification |
Installation
npm install @buildbase/sdk react@^19.0.0 react-dom@^19.0.0Quick Start
1. Import CSS
// app/layout.tsx (or your root layout)
import '@buildbase/sdk/css';2. Create Provider
// components/provider.tsx
'use client';
// BuildBaseProvider is an identical brand-aligned alias — prefer it in new code
import { SaaSOSProvider } from '@buildbase/sdk/react';
import { ApiVersion } from '@buildbase/sdk';
export default function SaaSProvider({ children }: { children: React.ReactNode }) {
return (
<SaaSOSProvider
serverUrl="https://your-api-server.com"
version={ApiVersion.V1}
orgId="your-org-id"
auth={{
clientId: 'your-client-id',
redirectUrl: 'http://localhost:3000',
callbacks: {
// Called on page refresh to restore session from httpOnly cookie
getSession: async () => {
const res = await fetch('/api/auth/session');
const data = await res.json();
return data.sessionId ?? null;
},
// Called after OAuth redirect to exchange code for sessionId
handleAuthentication: async code => {
const res = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
const data = await res.json();
return { sessionId: data.sessionId };
},
// Called on sign out to clear the httpOnly cookie
onSignOut: async () => {
await fetch('/api/auth/signout', { method: 'POST' });
window.location.reload();
},
handleEvent: (eventType, data) => {
console.log('SDK Event:', eventType, data);
},
onWorkspaceChange: async ({ workspace, user, role }) => {
console.log('Switching to:', workspace.name, 'as', role);
},
},
}}
>
{children}
</SaaSOSProvider>
);
}The SDK uses the same session pattern as next-auth: the session token lives in an httpOnly cookie (set by your server), and the SDK calls getSession() on page refresh to restore it. You need three server endpoints:
/api/auth/verify— exchanges OAuth code for sessionId, sets httpOnly cookie/api/auth/session— reads httpOnly cookie, returns{ sessionId }(called on page refresh)/api/auth/signout— clears the httpOnly cookie
3. Wrap Your App
// app/layout.tsx
import SaaSProvider from '@/components/provider';
import '@buildbase/sdk/css';
export default function RootLayout({ children }) {
return (
<html>
<body>
<SaaSProvider>{children}</SaaSProvider>
</body>
</html>
);
}4. Workspace Switcher
The WorkspaceSwitcher component uses a render prop pattern, giving you full control over the UI. Configure onWorkspaceChange in auth.callbacks (SaaSOSProvider) to handle workspace switches—used when clicking "Switch to" and when restoring from storage on page refresh. The callback receives { workspace, user, role } so you don't need to look up the user's role:
import React from 'react';
import { WorkspaceSwitcher } from '@buildbase/sdk/react';
function WorkspaceExample() {
return (
<WorkspaceSwitcher
trigger={(isLoading, currentWorkspace) => {
if (isLoading) {
return <div>Loading...</div>;
}
if (!currentWorkspace) {
return (
<div className="flex items-center gap-2 min-w-40 border rounded-md p-2 hover:bg-muted cursor-pointer">
<div className="bg-gray-200 flex aspect-square size-8 items-center justify-center rounded-lg"></div>
<div className="grid flex-1 text-left text-sm leading-tight">Choose a workspace</div>
</div>
);
}
return (
<div className="flex items-center gap-2 min-w-40 border rounded-md p-2 hover:bg-muted cursor-pointer">
<div className="flex items-center justify-center h-full w-full bg-muted rounded-lg max-h-8 max-w-8">
{currentWorkspace?.image && (
<img src={currentWorkspace?.image} alt={currentWorkspace?.name} />
)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{currentWorkspace?.name}</span>
</div>
</div>
);
}}
/>
);
}🎛️ UI Configuration
The optional ui prop on SaaSOSProvider controls which parts of the SDK UI are shown and lets you override individual UI strings. Everything defaults to visible/current behavior — the config can only hide UI and never bypasses platform permissions, which remain the security floor.
<SaaSOSProvider
serverUrl="..."
version="v1"
orgId="..."
ui={{
settings: {
// Hide whole sections of the workspace settings dialog.
// Hidden sections are also unreachable via deep links / defaultSection.
sections: { credits: false, notifications: false, 'connected-agents': false },
// Per-screen feature toggles
profile: { currency: false, timezone: false },
security: { passkeyDelete: false },
devices: { forget: false, sessions: false }, // hide "Remove" + the sessions block
general: { iconEditor: false },
users: { invite: false, seatPricing: false },
subscription: { cancel: false, invoicesTab: false, planDetails: false },
credits: { buyButton: false, transactions: false },
notifications: { push: false, emailToggles: false, pushToggles: false },
},
// Workspace switcher (client-side; ANDed with server settings)
workspaceSwitcher: { createButton: false, planBadge: false, memberCount: false },
// Behaviors that are otherwise automatic
behavior: {
autoOpenPlanDialog: false, // don't auto-open the plan picker when no subscription
trialEndingDays: 7, // global default for <WhenTrialEnding>
},
// Per-key string overrides, deep-merged over the active locale bundle
messages: {
settings: { sidebar: { credits: 'Tokens', subscription: 'Billing' } },
},
// Default fallback strings for the top-level error boundary
errorBoundary: { title: 'Oops!', retryLabel: 'Retry' },
// Date formatting for SDK-rendered dates (passkeys, connected agents, devices)
formats: { date: { dateStyle: 'short' } },
}}
>Section keys match the SettingsScreen values: profile, security, devices, connected-agents, general, users, subscription, usage, credits, features, notifications, permissions, danger. Hidden sections are removed from the sidebar (empty groups collapse) and unreachable via deep links or defaultSection — the dialog falls back to the first enabled section.
Per-dialog override — WorkspaceSettingsDialog accepts its own ui prop, deep-merged over the global config, so one app can render differently-configured dialogs:
<WorkspaceSettingsDialog
workspace={ws}
ui={{ settings: { sections: { credits: true, danger: false } } }}
/>In your own components, use the same single-call helper the SDK uses internally — it combines the ui config flag with a permission check:
import { useUIVisibility, Permission } from '@buildbase/sdk/react';
function MembersPanel() {
const { visible } = useUIVisibility();
// Config flag AND permission in one decision
if (!visible(ui => ui.settings?.users?.invite, Permission.WORKSPACE_MEMBERS_INVITE)) {
return null;
}
return <InviteForm />;
}useUIConfig() gives you the raw ui object if you only need the config values; mergeUIConfig(base, override) is the deep-merge used for per-dialog overrides.
📖 Full guide with the complete toggle reference, precedence rules, and recipes (single-tenant, external billing, white-label, read-only): docs/UI-CONFIG.md
🔐 Authentication
Authentication Hook
Use the useSaaSAuth hook to manage authentication state and actions:
import { useSaaSAuth } from '@buildbase/sdk/react';
function AuthExample() {
const { user, isAuthenticated, signIn, signOut, status } = useSaaSAuth();
return (
<div>
{!isAuthenticated ? (
<div>
<h1>Welcome! Please sign in</h1>
<button onClick={() => signIn()} disabled={status === 'loading'}>
{status === 'loading' ? 'Signing in...' : 'Sign In'}
</button>
</div>
) : (
<div>
<h1>Welcome back, {user?.name}!</h1>
<p>Email: {user?.email}</p>
<p>Role: {user?.role}</p>
<button onClick={signOut}>Sign Out</button>
</div>
)}
</div>
);
}Authentication Hook Properties
const {
user, // Current user object (null if not authenticated)
session, // Full session object with user and sessionId
isAuthenticated, // Boolean: true if user is authenticated
isLoading, // Boolean: true when checking authentication status
isRedirecting, // Boolean: true when redirecting for OAuth
status, // AuthStatus: 'loading' | 'redirecting' | 'authenticating' | 'authenticated' | 'unauthenticated' (use AuthStatus enum for type-safe checks)
signIn, // Function: initiates sign-in flow. Accepts optional returnUrl to redirect back after login.
signOut, // Function: signs out the user
openWorkspaceSettings, // Function: opens workspace settings dialog to a specific section
} = useSaaSAuth();Workspace Settings Sections
Open the workspace settings dialog to a specific section:
openWorkspaceSettings('profile'); // Account profile
openWorkspaceSettings('general'); // Workspace name, icon
openWorkspaceSettings('users'); // Workspace members
openWorkspaceSettings('subscription'); // Plan & Billing
openWorkspaceSettings('usage'); // Quota usage dashboard
openWorkspaceSettings('features'); // Feature toggles
openWorkspaceSettings('connected-agents'); // Connected AI agents + connect guide
openWorkspaceSettings('danger'); // Delete workspace (owner only)Some sections accept an optional second action argument to deep-link past the screen. For connected agents, open the setup guide directly:
openWorkspaceSettings('connected-agents', { action: 'openConnectGuide' });Authentication Components
For declarative rendering, use the conditional components:
import { WhenAuthenticated, WhenUnauthenticated } from '@buildbase/sdk/react';
function App() {
return (
<div>
<WhenUnauthenticated>
<LoginPage />
</WhenUnauthenticated>
<WhenAuthenticated>
<Dashboard />
</WhenAuthenticated>
</div>
);
}Redirect Preservation
The SDK automatically preserves the URL when signIn() is called. After login, the user is redirected back to the page they were on.
// Automatic — just call signIn(), the current URL is saved
signIn();
// Custom — pass a specific URL to redirect to after login
signIn('https://app.com/dashboard?bb=action:selectPlan,plan:abc');This works across the full OAuth round-trip via localStorage (10-minute TTL, validated with validateRedirectUrl()).
For advanced use cases, the low-level helpers are also exported:
import { saveAuthIntent, consumeAuthIntent, clearAuthIntent } from '@buildbase/sdk';Affiliate / Referral Tracking
Pass affiliate/referral data to Stripe checkout sessions via the getCheckoutStripeParams prop on SaaSOSProvider. This async callback is called before every checkout session is created, so you can fetch referral IDs, read cookies, or call any async API:
<SaaSOSProvider
serverUrl="https://your-api-server.com"
version={ApiVersion.V1}
orgId="your-org-id"
auth={authConfig}
getCheckoutStripeParams={async request => {
// Rewardful, FirstPromoter, PartnerStack — read client_reference_id
const referralId = await getRewardfulReferralId();
return {
clientReferenceId: referralId,
// Endorsely — reads subscription metadata
subscriptionMetadata: { endorsely_referral: window.endorsely_referral },
// Custom tracking on the checkout session
metadata: { campaign: 'summer-sale' },
};
}}
>
<App />
</SaaSOSProvider>The returned object is merged into the Stripe checkout session. You can return any combination of the fields below, or undefined to proceed without extra options:
| Field | Stripe mapping | Use case |
| ---------------------- | ----------------------------- | -------------------------------------- |
| clientReferenceId | client_reference_id | Rewardful, FirstPromoter, etc. |
| metadata | metadata (checkout session) | Custom tracking, Endorsely |
| subscriptionMetadata | subscription_data.metadata | Data that persists on the subscription |
👥 Role-Based Access Control
Role Components
Control access based on user roles:
import { WhenRoles, WhenWorkspaceRoles } from '@buildbase/sdk/react';
function AdminPanel() {
return (
<div>
{/* Global user roles */}
<WhenRoles roles={['admin', 'super-admin']}>
<AdminControls />
</WhenRoles>
{/* Workspace-specific roles */}
<WhenWorkspaceRoles roles={['owner', 'admin']}>
<WorkspaceSettings />
</WhenWorkspaceRoles>
{/* With fallback content */}
<WhenRoles roles={['admin']} fallback={<p>You need admin access to view this content</p>}>
<SensitiveData />
</WhenRoles>
</div>
);
}🎛️ Feature Flags
Control feature visibility based on workspace and user settings:
import {
WhenWorkspaceFeatureEnabled,
WhenWorkspaceFeatureDisabled,
WhenUserFeatureEnabled,
WhenUserFeatureDisabled,
} from '@buildbase/sdk/react';
function FeatureExample() {
return (
<div>
{/* Workspace-level features */}
<WhenWorkspaceFeatureEnabled slug="advanced-analytics">
<AdvancedAnalytics />
</WhenWorkspaceFeatureEnabled>
<WhenWorkspaceFeatureDisabled slug="beta-features">
<p>Beta features are not enabled for this workspace</p>
</WhenWorkspaceFeatureDisabled>
{/* User-level features */}
<WhenUserFeatureEnabled slug="premium-features">
<PremiumDashboard />
</WhenUserFeatureEnabled>
<WhenUserFeatureDisabled slug="trial-mode">
<UpgradePrompt />
</WhenUserFeatureDisabled>
</div>
);
}Feature Flags Hook
Use the useUserFeatures hook to check feature flags programmatically:
import { useUserFeatures } from '@buildbase/sdk/react';
function FeatureCheck() {
// { features, isFeatureEnabled, loading, error, refetch }
// (the older `isLoading` / `refreshFeatures` names still work but are deprecated)
const { features, isFeatureEnabled, refetch } = useUserFeatures();
return (
<div>{isFeatureEnabled('premium-features') ? <PremiumContent /> : <StandardContent />}</div>
);
}📋 Subscription Gates
Control UI visibility based on the current workspace’s subscription. Subscription data is loaded once per workspace and refetched when the workspace changes or when the subscription is updated (e.g. upgrade, cancel, resume).
SubscriptionContextProvider is included in SaaSOSProvider by default, so subscription gates work without extra setup.
Subscription Gate Components
import {
WhenSubscription,
WhenNoSubscription,
WhenSubscriptionToPlans,
} from '@buildbase/sdk/react';
function BillingExample() {
return (
<div>
{/* Show when workspace has any active subscription */}
<WhenSubscription>
<BillingSettings />
</WhenSubscription>
{/* Show when workspace has no subscription */}
<WhenNoSubscription>
<UpgradePrompt />
</WhenNoSubscription>
{/* Show only when subscribed to specific plans (by slug, case-insensitive) */}
<WhenSubscriptionToPlans plans={['pro', 'enterprise']}>
<AdvancedAnalytics />
</WhenSubscriptionToPlans>
</div>
);
}| Component | Renders when |
| ------------------------- | -------------------------------------------------------------------- |
| WhenSubscription | Current workspace has an active subscription (any plan); not loading |
| WhenNoSubscription | Current workspace has no subscription (or no workspace); not loading |
| WhenSubscriptionToPlans | Current workspace is subscribed to one of the given plan slugs |
All gates must be used inside SubscriptionContextProvider (included in SaaSOSProvider). By default they return null while loading or when the condition is not met. You can pass optional loadingComponent (component/element to show while loading) and fallbackComponent (component/element to show when condition is not met):
<WhenSubscription
loadingComponent={<Skeleton className="h-20" />}
fallbackComponent={<UpgradePrompt />}
>
<BillingSettings />
</WhenSubscription>
<WhenSubscriptionToPlans
plans={['pro', 'enterprise']}
loadingComponent={<Spinner />}
fallbackComponent={<p>Upgrade to Pro or Enterprise to access this feature.</p>}
>
<AdvancedAnalytics />
</WhenSubscriptionToPlans>useSubscriptionContext
Use the hook when you need subscription data or a manual refetch (e.g. after returning from Stripe checkout):
import { useSubscriptionContext } from '@buildbase/sdk/react';
function SubscriptionStatus() {
const { response, loading, refetch } = useSubscriptionContext();
if (loading) return <Spinner />;
if (!response?.subscription) return <p>No active subscription</p>;
const plan = response.plan ?? response.subscription?.plan;
return (
<div>
<p>Plan: {plan?.name ?? plan?.slug}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}| Property | Type | Description |
| ---------- | ------------------------------- | ------------------------------------------------------ |
| response | ISubscriptionResponse \| null | Current subscription data for the current workspace |
| loading | boolean | True while subscription is being fetched |
| refetch | () => Promise<void> | Manually refetch subscription (e.g. after plan change) |
When subscription refetches
- When the current workspace changes (automatic).
- When subscription is updated via SDK (e.g.
useUpdateSubscription, cancel, resume) — refetch is triggered automatically. - When you call
refetch()(e.g. after redirect from checkout).
⏳ Trial Gates
Control UI based on trial state. Works with Stripe-native trials (both card-required and no-card).
Trial Gate Components
import { WhenTrialing, WhenNotTrialing, WhenTrialEnding } from '@buildbase/sdk/react';
function TrialExample() {
return (
<div>
{/* Show only during active trial */}
<WhenTrialing>
<TrialBanner />
</WhenTrialing>
{/* Show when NOT trialing (active, canceled, or no subscription) */}
<WhenNotTrialing>
<RegularContent />
</WhenNotTrialing>
{/* Show when trial ends within N days (default: 3) */}
<WhenTrialEnding daysThreshold={7}>
<UpgradeUrgentBanner />
</WhenTrialEnding>
</div>
);
}| Component | Renders when |
| ----------------- | --------------------------------------------------------------- |
| WhenTrialing | Subscription status is trialing |
| WhenNotTrialing | Subscription status is NOT trialing |
| WhenTrialEnding | Trialing AND trial ends within daysThreshold days (default 3) |
All trial gates support loadingComponent and fallbackComponent props.
useTrialStatus
Hook that computes trial information from the subscription context:
import { useTrialStatus } from '@buildbase/sdk/react';
function TrialInfo() {
const { isTrialing, daysRemaining, trialEndsAt, isTrialEnding } = useTrialStatus();
if (!isTrialing) return null;
return (
<div>
<p>Trial ends in {daysRemaining} days</p>
{isTrialEnding && <p>Upgrade now to keep access!</p>}
</div>
);
}| Property | Type | Description |
| ---------------- | -------------- | ------------------------------------------------- |
| isTrialing | boolean | Whether subscription is in trial |
| daysRemaining | number | Days left in trial (0 if not trialing or expired) |
| trialEndsAt | Date \| null | Trial end date |
| trialStartedAt | Date \| null | Trial start date |
| isTrialEnding | boolean | True when 3 or fewer days remaining |
🔔 Push Notifications
Browser push notifications — built into the SDK. Users can enable/disable from the Notifications tab in workspace settings. Billing events (payment failed, trial ending) auto-send push notifications.
Only setup required: Create public/push-sw.js in your app:
self.addEventListener('push', function (event) {
if (!event.data) return;
try {
var payload = event.data.json();
var options = {
body: payload.body || '',
icon: payload.icon || undefined,
badge: payload.badge || payload.icon || undefined,
image: payload.image || undefined,
tag: payload.tag || undefined,
actions: payload.actions || undefined,
silent: payload.silent || false,
requireInteraction: payload.requireInteraction || false,
renotify: payload.renotify || false,
timestamp: payload.timestamp || undefined,
dir: payload.dir || 'auto',
data: { url: payload.url, ...(payload.data || {}) },
};
event.waitUntil(self.registration.showNotification(payload.title || 'Notification', options));
} catch (e) {
console.error('[PushSW]', e);
}
});
self.addEventListener('notificationclick', function (event) {
event.notification.close();
var url = event.notification.data && event.notification.data.url;
if (url) {
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
for (var i = 0; i < list.length; i++) {
if (list[i].url === url && 'focus' in list[i]) return list[i].focus();
}
if (clients.openWindow) return clients.openWindow(url);
})
);
}
});Everything else is built-in — permission handling, subscribe/unsubscribe, settings UI, billing auto-triggers, and browser-specific unblock instructions.
📬 Notifications
Send email and push notifications to workspace members. The system has three layers:
- System notifications — Automatically triggered by platform events (workspace invite, payment failed, trial ending, etc.). Managed by the developer in the admin dashboard.
- Custom notifications — Defined by the developer, triggered from app code via SDK. Can be made user-configurable.
- Ad-hoc notifications — Send push notifications with any event slug without pre-registering it. No event setup needed — just pass
title,message, and optionallyicon,image,url. Email requires a registered event with a linked template.
Sending Notifications (Server-Side)
import { notification } from '@/lib/buildbase';
// Notify a specific user
await notification.send(workspaceId, 'comment_added', userId, {
title: 'New Comment', // Push title (falls back to event name)
message: 'Alice commented on your project', // Push body + email {{message}}
icon: 'https://example.com/comment-icon.png', // Custom push icon (falls back to org icon)
image: 'https://example.com/screenshot.jpg', // Large image in push notification body
url: 'https://app.example.com/projects/123#comments', // Opens on push click + {{url}} in email
});
// Notify all workspace members (omit userId)
await notification.send(workspaceId, 'new_release', undefined, {
title: 'New Release',
message: 'Version 2.0 is now available with dark mode and API v2!',
image: 'https://example.com/release-banner.jpg',
url: 'https://app.example.com/changelog',
});Ad-hoc Notifications
Send push notifications without creating a custom event first — any event slug works:
// No need to register 'deployment_success' as a custom event
await notification.send(workspaceId, 'deployment_success', userId, {
title: 'Deployment Complete',
message: 'v2.1.0 deployed to production',
icon: 'https://example.com/deploy-icon.png',
url: '/deployments/latest',
channels: { push: true },
});Note: Ad-hoc events support push only. Email requires a registered event with a linked email template.
Push Options
Fine-grained control over push notification behavior:
// Action buttons — user can tap "Reply" or "Dismiss" directly on the notification
await notification.send(workspaceId, 'new_message', userId, {
title: 'New message from Alice',
message: 'Hey, are you free for a call?',
actions: [
{ action: 'reply', title: 'Reply', icon: 'https://example.com/reply.png' },
{ action: 'dismiss', title: 'Dismiss' },
],
tag: 'chat-alice', // Replaces previous "chat-alice" notification instead of stacking
renotify: true, // Still vibrate/sound when replacing
channels: { push: true },
});
// Critical alert — stays visible until user interacts
await notification.send(workspaceId, 'payment_failed', userId, {
title: 'Payment Failed',
message: 'Your subscription will be suspended in 3 days',
badge: 'https://example.com/alert-badge.png',
requireInteraction: true, // No auto-dismiss
urgency: 'high', // Prioritized delivery on mobile
channels: { push: true },
});
// Silent notification — no sound or vibration
await notification.send(workspaceId, 'sync_complete', userId, {
title: 'Sync Complete',
message: '1,234 records synced',
silent: true,
urgency: 'low',
channels: { push: true },
});| Option | Type | Description |
| -------------------- | ------------------------------- | ---------------------------------------------------------------- |
| badge | string | Small monochrome icon for status bar (Android/ChromeOS) |
| tag | string | Replaces existing notification with same tag instead of stacking |
| actions | Array<{action, title, icon?}> | Up to 2 action buttons on the notification |
| silent | boolean | No sound or vibration |
| requireInteraction | boolean | Stays visible until user interacts |
| renotify | boolean | Sound/vibrate again when replacing via tag |
| timestamp | number | Custom timestamp (ms since epoch) shown on notification |
| dir | 'ltr' \| 'rtl' \| 'auto' | Text direction for title/body |
Delivery Options
Control how and when the push service delivers the notification:
// Schedule for later
await notification.send(workspaceId, 'daily_digest', undefined, {
title: 'Your Daily Digest',
message: '12 new updates in your workspace',
scheduledAt: '2026-04-16T09:00:00Z', // Deliver at 9am UTC tomorrow
channels: { push: true },
});
// Short-lived notification — discard if not delivered in 1 hour
await notification.send(workspaceId, 'flash_sale', undefined, {
title: 'Flash Sale — 50% off!',
message: 'Ends in 1 hour',
image: 'https://example.com/sale-banner.jpg',
ttl: 3600, // Expires after 1 hour (seconds)
urgency: 'high', // Deliver ASAP
channels: { push: true },
});| Option | Type | Description |
| ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------- |
| scheduledAt | string | ISO 8601 date. Delays delivery until the specified time |
| ttl | number | Time-to-live in seconds. Push service discards if not delivered in time. Default: 86400 (24h) |
| urgency | 'very-low' \| 'low' \| 'normal' \| 'high' | Delivery priority hint. Affects battery usage on mobile |
Channel Control
By default, both email and push are sent (based on event config). Override per-send with channels:
// Only push — real-time alert, no email
await notification.send(workspaceId, 'typing_indicator', userId, {
message: 'Alice is typing...',
channels: { push: true },
});
// Only email — digest or report, no push
await notification.send(workspaceId, 'weekly_report', undefined, {
message: 'Your weekly activity report is ready',
channels: { email: true },
});
// Both channels explicitly
await notification.send(workspaceId, 'comment_added', userId, {
message: 'New comment on your project',
channels: { email: true, push: true },
});Note: Even with
channelsoverride, the 4-layer gate still applies. If the admin disabled push globally,channels: { push: true }won't send push.
Merge Tags
Both email and push support merge tags with {{tag}} syntax:
await notification.send(workspaceId, 'export_ready', userId, {
title: '{{workspaceName}} — Export Ready', // → "Acme Corp — Export Ready"
message: 'Hi {{name}}, your export is ready', // → "Hi Alice, your export is ready"
downloadUrl: 'https://example.com/exports/123',
fileName: 'report.csv',
});| Tag | Resolves to | Available in |
| ------------------- | ------------------------ | ---------------------------------- |
| {{name}} | Recipient's name | Email + Push |
| {{email}} | Recipient's email | Email + Push |
| {{workspaceName}} | Workspace name | Email + Push |
| {{message}} | The message field | Email template |
| {{url}} | The url field | Email template + Push click target |
| {{anyKey}} | Value from data object | Email + Push |
Response
{
sent: true,
channels: { email: true, push: true },
notifiedCount: 5 // Number of users notified (1 for single user, N for workspace)
}How It Works
When notification.send() is called, the system checks 4 layers before delivering:
- Org global settings — Developer can disable all email or all push notifications globally
- Event config — Per-event enabled/disabled and per-channel (email/push) toggles
- Workspace preferences — End-user overrides (only for events marked
userManaged) - User unsubscribe — Per-user email unsubscribe preferences (checked at delivery time)
Creating Custom Events
Custom notification events are created in the admin dashboard under Notifications > Custom:
- Name — Display name (e.g., "Comment Added")
- Slug — Used in code (e.g.,
comment_added) - Category — Grouping in settings UI (e.g., "Activity")
- Channels — Enable/disable email and push per event
- User Control — If enabled, workspace members can toggle this notification in their settings
An email template is auto-created for each custom event. Edit it in Email Templates to customize the content and add merge tags like {{downloadUrl}}, {{commentText}}, etc.
Notification Settings (End-User UI)
The workspace settings panel shows notification preferences automatically — only for events where the developer enabled "User Control". System notifications are never shown to end users.
import { SaaSOSProvider } from '@buildbase/sdk/react';
// The Notifications tab in workspace settings shows:
// - Browser push toggle (subscribe/unsubscribe)
// - Per-event email/push toggles (only user-manageable custom events)Notification Types
import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk';
interface NotificationData {
title?: string; // Push title (falls back to event name)
message?: string; // Push body + email {{message}}
icon?: string; // Custom push icon URL (falls back to org icon)
image?: string; // Large image in push notification body
badge?: string; // Small monochrome status bar icon
url?: string; // Opens on push click + {{url}} in email
tag?: string; // Replace instead of stack notifications
actions?: Array<{ action; title; icon? }>; // Action buttons (max 2)
silent?: boolean; // No sound/vibration
requireInteraction?: boolean; // No auto-dismiss
renotify?: boolean; // Re-alert on tag replace
timestamp?: number; // Custom timestamp (ms)
dir?: 'ltr' | 'rtl' | 'auto'; // Text direction
ttl?: number; // Time-to-live (seconds)
urgency?: 'very-low' | 'low' | 'normal' | 'high'; // Delivery priority
scheduledAt?: string; // ISO 8601 delayed delivery
channels?: { email?: boolean; push?: boolean }; // Override which channels to use
[key: string]: any; // Custom merge tags for email + push
}
interface NotificationResult {
sent: boolean;
channels: { email: boolean; push: boolean };
notifiedCount?: number;
reason?: string; // Only when sent=false
}
interface NotificationEvent {
slug: string;
name: string;
description: string;
category: string;
channels: { email: boolean; push: boolean };
}🌐 Internationalization (i18n)
The SDK supports 8 locales with ICU MessageFormat for plurals, selects, and number formatting.
Setup
<SaaSOSProvider locale="hi">{/* All SDK UI renders in Hindi */}</SaaSOSProvider>Supported Locales
| Code | Language | Numerals | Direction |
| ---- | -------- | ------------------------- | --------- |
| en | English | 1,234.56 | LTR |
| es | Spanish | 1.234,56 | LTR |
| fr | French | 1 234,56 | LTR |
| de | German | 1.234,56 | LTR |
| ja | Japanese | 1,234.56 | LTR |
| zh | Chinese | 1,234.56 | LTR |
| hi | Hindi | Devanagari (e.g. 1,234) | LTR |
| ar | Arabic | Arabic-Indic (e.g. 1,234) | RTL |
useTranslation Hook
import { useTranslation } from '@buildbase/sdk/react';
function MyComponent() {
const { t, locale, dir, fmtNum, fmtCents } = useTranslation();
return (
<div dir={dir}>
<p>{t('subscription.currentPlan')}</p> {/* Type-safe key lookup */}
<p>{t('users.memberCount', { count: 5 })}</p> {/* ICU plural: "5 members" */}
<p>{fmtNum(1234)}</p> {/* Locale-aware: "1,234" or "1,234" */}
<p>{fmtCents(1999, 'usd')}</p> {/* "$19.99" or "19.99 US$" */}
</div>
);
}Features
- ICU MessageFormat — plurals (
{count, plural, one {# item} other {# items}}), selects, number formatting - Type-safe keys —
TranslationKeyunion type with autocomplete, catches typos at compile time - Native numerals — Hindi uses Devanagari digits, Arabic uses Arabic-Indic digits
- RTL support —
dirattribute on all dialogs, logical CSS properties (start/end instead of left/right) - Locale-aware formatting — dates, currencies, and numbers formatted per locale
- Memoized Intl formatters — shared
Intl.NumberFormat/DateTimeFormat/PluralRulesinstances for performance - Lazy-loaded translations — non-English locales loaded on demand, English always bundled
🏠 Workspace Modes
The SDK supports two workspace modes, configured from the admin dashboard (no code changes needed):
Personal Mode
For B2C solo tools (Todoist, Grammarly, personal dashboards):
- 1 user = 1 auto-created workspace
- No team invites, no workspace switcher
- Clicking workspace trigger opens Settings directly
- Seats, members, roles sections hidden in UI
- Enforced at API level — workspace creation and invites blocked
Platform Mode (Default)
For full SaaS platforms (Slack, GitHub, Discord):
- Multi-workspace, multi-user
- Create workspaces, invite members, switch between them
- Full settings UI with members, roles, billing, seats
Advanced Overrides
Platform mode supports granular overrides from the admin dashboard:
| Setting | Options | Default | | --------------------------- | -------------------------------- | -------- | | Can Create Workspace | Everyone / Owner Only / Disabled | Everyone | | Can Invite Members | Everyone / Admin Only / Disabled | Everyone | | Show Workspace Switcher | On / Off | On | | Max Workspaces Per User | 0 (unlimited) or a number | 0 | | Auto-Create First Workspace | On / Off | On |
These let you achieve team-like, managed, or enterprise-like behavior without a separate mode.
🔌 Connected Agents
Let users connect AI agents to their account and manage them. <ConnectedAgents /> is a ready-made screen (also a workspace settings section) that lists the agents a user has authorized — each with a Disconnect action — and, when you provide an MCP server address, shows a Connect an agent button that opens a plain-language setup guide.
Enable the connect guide
Pass an mcp config to the provider. Without it the screen still lists and revokes agents; with it, the "Connect an agent" button and setup dialog appear.
<SaaSOSProvider
// ...serverUrl, orgId, auth
mcp={{
url: 'https://app.example.com/api/mcp', // your MCP server endpoint (required to show the guide)
name: 'Acme', // friendly name used in prose + config snippets
docsUrl: 'https://docs.example.com/agents', // optional "Learn more" link
}}
>The dialog walks users through the major AI apps — ChatGPT, Claude, Cursor, VS Code, Windsurf, Cline — each with the exact click-path or config-file snippet (your server URL is filled in for them), plus a copy-and-paste prompt for chat assistants. It's fully translated (all 8 locales), RTL-aware, and responsive (full-screen on mobile, centered card on desktop).
The screen
import { ConnectedAgents } from '@buildbase/sdk/react';
function AgentsPage() {
return <ConnectedAgents />;
}Props: title, description, disconnectLabel, emptyLabel (all default to translated strings; pass null to hide the heading) and showConnectGuide (default true — set false to hide the connect button even when mcp is configured).
Because it's a settings section, you can also open it programmatically:
const { openWorkspaceSettings } = useSaaSAuth();
openWorkspaceSettings('connected-agents'); // the screen (list + connect button)
openWorkspaceSettings('connected-agents', { action: 'openConnectGuide' }); // straight to the setup dialogThe same deep link works via URL: ?bb=action:openConnectGuide.
Custom UI
useConnectedAgents()— headless{ agents, loading, error, revoking, refresh, revoke }for a fully custom list.useMcpConnection()— themcpconfig you passed (ornull), for building your own guide.<ConnectMcpGuide />— the guide body on its own (copyable server URL + paste-in prompt + per-client accordion), embeddable anywhere. Override the app list viamcp.clientsand the prompt viamcp.prompt(falsehides it).fillMcpTemplate/mcpServerKey— the placeholder helpers ({{url}}/{{name}}/{{key}}) for building custom client snippets.
📱 Devices & Sessions
Let users see and manage where they're signed in. Two ready-made screens (also combined into one workspace settings section, devices):
<Devices />— the devices the user has signed in from, each showing a friendly "Browser · OS" line, location + IP, last-used time, and a "This device" / "Trusted" / notifications badge. Per-row actions: Rename, Sign out (revoke that device's live sessions), and Remove (forget the device, with a confirm dialog).<Sessions />— the user's currently-active sessions, each with a per-row Sign out (the current session is badged, not revocable here).
Both are session-authed and scoped to the signed-in user — no config beyond wrapping your app in <SaaSOSProvider>. For the "This device" flag and device binding to work, the SDK automatically fetches a server-signed device token after login (and on every session refresh) and echoes it as a persisted x-device-id header — no configuration and nothing to return from your auth callbacks.
The screens
import { Devices, Sessions } from '@buildbase/sdk/react';
function SecurityPage() {
return (
<div className="space-y-8">
<Devices />
<Sessions />
</div>
);
}Or open the combined settings section programmatically:
const { openWorkspaceSettings } = useSaaSAuth();
openWorkspaceSettings('devices'); // "Devices & sessions" screenShow/hide actions & override labels
Every action can be hidden and every button relabeled — an explicit prop wins, otherwise it falls back to the provider ui.settings.devices.* config (visible unless set to false).
<Devices
title={null} // hide the heading (like every screen; also `description`)
showRename={false} // hide per-row actions
showSignOut={false}
showRemove={false}
renameLabel="Edit" // relabel buttons (default to translated strings)
signOutLabel="Log out"
removeLabel="Delete"
emptyLabel="No devices yet"
/>
<Sessions showSignOut={false} signOutLabel="End session" emptyLabel="No other sessions" />Provider-level equivalent (applies everywhere, incl. the settings screen):
<SaaSOSProvider ui={{ settings: {
sections: { devices: false }, // hide the whole "Devices & sessions" settings section
devices: {
rename: false, // device Rename
signOut: false, // device Sign out
forget: false, // device Remove
sessions: false, // the active-sessions block on the settings screen
sessionSignOut: false // per-session Sign out
},
}}}>Custom UI
useDevices()— headless{ devices, loading, error, busyId, refresh, rename, signOut, forget }.useSessions()— headless{ sessions, loading, error, revoking, refresh, revoke }.DevicesApi/SessionsApi(+IDeviceView/ISessionViewtypes) for direct calls.
Server-side
import BuildBase from '@buildbase/sdk';
const bb = BuildBase({ serverUrl, orgId, getSessionId });
await bb.devices.list(); // IDeviceView[]
await bb.devices.rename(deviceId, name);
await bb.devices.signOut(deviceId); // revoke the device's sessions
await bb.devices.forget(deviceId); // sign out + remove the row
await bb.sessions.list(); // ISessionView[]
await bb.sessions.revoke(sessionId); // by the session's public handle👤 User Management
User Attributes
Manage custom user attributes (key-value pairs):
import { useUserAttributes } from '@buildbase/sdk/react';
function UserProfile() {
// { attributes, loading, error, refetch, updateAttribute, updateAttributes }
// (the older `isLoading` / `refreshAttributes` names still work but are deprecated)
const { attributes, loading, updateAttribute, updateAttributes, refetch } = useUserAttributes();
const handleUpdate = async () => {
// Update single attribute
await updateAttribute('theme', 'dark');
// Or update multiple attributes
await updateAttributes({
theme: 'dark',
notifications: true,
language: 'en',
});
};
return (
<div>
<p>Theme: {attributes.theme}</p>
<button onClick={handleUpdate}>Update Preferences</button>
</div>
);
}🏢 Complete Workspace Management
The useSaaSWorkspaces hook provides comprehensive workspace management:
import { useSaaSWorkspaces } from '@buildbase/sdk/react';
function WorkspaceManager() {
const {
workspaces, // Array of all workspaces
currentWorkspace, // Currently selected workspace
loading, // Loading state
refreshing, // Refreshing state
switching, // True when a workspace switch is in progress
switchingToId, // Workspace ID currently being switched to (null when not switching)
error, // Error message
fetchWorkspaces, // Fetch all workspaces
refreshWorkspaces, // Background refresh
setCurrentWorkspace, // Direct workspace set (bypasses onWorkspaceChange)
switchToWorkspace, // Full switch flow: onWorkspaceChange first, then set workspace
createWorkspace, // Create new workspace
updateWorkspace, // Update workspace
deleteWorkspace, // Delete workspace
getUsers, // Get workspace users
addUser, // Add user to workspace
removeUser, // Remove user from workspace
updateUser, // Update user role/permissions
getFeatures, // Get all available features
updateFeature, // Toggle workspace feature
getProfile, // Get current user profile
updateUserProfile, // Update user profile
updateWorkspaceSettings, // Update workspace settings
updateWorkspacePermissions, // Update workspace permissions
} = useSaaSWorkspaces();
// Example: Create a workspace
const handleCreate = async () => {
await createWorkspace('My Workspace', 'https://example.com/logo.png');
};
// Example: Add user to workspace
const handleAddUser = async () => {
await addUser(currentWorkspace._id, '[email protected]', 'member');
};
return <div>{/* Your workspace UI */}</div>;
}💰 Public Pricing (No Login)
Display subscription plans and pricing on public pages (e.g. marketing site, pricing page) without requiring users to log in.
usePublicPlans
Fetches public plans by slug. Returns items (features, limits, quotas) and plans (with pricing). You construct the layout from this data:
import { usePublicPlans } from '@buildbase/sdk/react';
function PublicPricingPage() {
const { items, plans, loading, error } = usePublicPlans('main-pricing');
if (loading) return <Loading />;
if (error) return <Error message={error} />;
return (
<div>
{plans.map(plan => (
<PlanCard key={plan._id} plan={plan} items={items} />
))}
</div>
);
}PricingPage Component
Use the PricingPage component with a render-prop pattern:
import { PricingPage } from '@buildbase/sdk/react';
function PublicPricingPage() {
return (
<PricingPage slug="main-pricing" redirectBaseUrl="https://app.com/dashboard">
{({ loading, error, items, plans, selectPlan, refetch }) => {
if (loading) return <Loading />;
if (error) return <Error message={error} />;
return (
<div>
{plans.map(plan => (
<div key={plan._id}>
<PlanCard plan={plan} items={items} />
<button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
{plan.trial?.enabled
? `Start ${plan.trial.durationDays}-Day Trial`
: 'Select Plan'}
</button>
</div>
))}
</div>
);
}}
</PricingPage>
);
}selectPlan() handles everything automatically:
- Authenticated → opens the "Choose Your Plan" dialog
- Not authenticated → saves a redirect URL, triggers sign-in, and after login the user lands on the dashboard with the plan picker dialog open
| Prop | Type | Description |
| ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| slug | string | Plan group slug (e.g. 'main-pricing', 'enterprise') |
| children | (details) => ReactNode | Render prop receiving plan details (see below) |
| redirectBaseUrl | string | Base URL for post-login redirects (e.g. "https://app.com/dashboard"). Enables selectPlan() for unauthenticated users. |
| loadingFallback | ReactNode | Custom loading UI (defaults to skeleton) |
| errorFallback | (error: string) => ReactNode | Custom error UI |
Render prop details: { loading, error, items, plans, notes, refetch, selectPlan }
selectPlan(planVersionId, interval, currency)— One-call plan selection (handles auth + dialog automatically)
Response shape: items = subscription item definitions (features, limits, quotas with category); plans = plan versions with pricing, quotas, features, limits.
Backend requirement: GET /api/v1/public/{orgId}/plans/{groupSlug} must be implemented and allow unauthenticated access.
💱 Multi-Currency & Pricing Utilities
Plans support pricing variants (multi-currency). Use these utilities for display and lookup.
Currency utilities
| Export | Purpose |
| -------------------------------------------------- | --------------------------------------------------------------- |
| CURRENCY_DISPLAY | Map of currency code → symbol (e.g. usd → $) |
| CURRENCY_FLAG | Map of currency code → flag emoji |
| PLAN_CURRENCY_CODES | Allowed billing currency codes (for dropdowns/validation) |
| PLAN_CURRENCY_OPTIONS | Options array for plan currency selects |
| getCurrencySymbol(currency) | Symbol for a Stripe currency code |
| getCurrencyFlag(currency) | Flag emoji for a currency code |
| formatCents(cents, currency) | Format a minor-unit amount with the symbol table |
| formatMinorAmountIntl(amount, currency, locale?) | Locale-aware Intl money formatting of minor units |
| getCurrencyDecimals(currency) | ISO 4217 minor-unit digits (0 JPY, 2 USD, 3 KWD; CLDR fallback) |
| isZeroDecimalCurrency(currency) | True when the currency has no minor unit (JPY, KRW, …) |
| minorAmountToDisplay(amount, currency) | Minor units → display number string (no symbol) |
| formatOverageRate(cents, currency) | Format overage rate for display |
| formatOverageRateWithLabel(...) | Overage rate with optional unit label |
| formatQuotaIncludedOverage(...) | "X included, then $Y / unit" style text |
| getQuotaUnitLabelFromName(name) | Human-readable unit label from quota name |
Pricing variant utilities
| Export | Purpose |
| ------------------------------------------------------------------------ | ------------------------------------------------------------ |
| getPricingVariant(planVersion, currency) | Get variant for a currency, or null |
| getBasePriceCents(planVersion, currency, interval) | Base price in cents for currency/interval |
| getStripePriceIdForInterval(planVersion, currency, interval) | Stripe price ID for checkout |
| getQuotaOverageCents(planVersion, currency, quotaSlug, interval) | Overage cents for a quota |
| getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval) | Display value with overage for a variant |
| getAvailableCurrenciesFromPlans(plans) | Unique currency codes across plan versions |
| getDisplayCurrency(planVersion, currency) | Display currency (variant exists ? currency : plan.currency) |
| getBillingIntervalAndCurrencyFromPriceId(planVersions, priceId) | Resolve price ID to interval + currency |
Types: IPricingVariant, PlanVersionWithPricingVariants, QuotaDisplayWithOverage.
Quota utilities
| Export | Purpose
