JSPM

  • Created
  • Published
  • Downloads 136
  • Score
    100M100P100Q76438F
  • License MIT

A SDK for Buildbase

Package Exports

  • @buildbase/sdk
  • @buildbase/sdk/css
  • @buildbase/sdk/data
  • @buildbase/sdk/react

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

  • πŸ” 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, WhenTrialEnding components + useTrialStatus hook
  • πŸ”” Push Notifications - Browser push notifications with usePushNotifications hook, 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
  • πŸ’Ί 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 getCheckoutStripeParams prop (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
  • πŸ–₯️ 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

Installation

npm install @buildbase/sdk react@^19.0.0 react-dom@^19.0.0

Quick Start

1. Import CSS

// app/layout.tsx (or your root layout)
import '@buildbase/sdk/css';

2. Create Provider

// components/provider.tsx
'use client';

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 Management

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>
        );
      }}
    />
  );
}

πŸ” 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('danger'); // Delete workspace (owner only)

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() {
  const { features, isFeatureEnabled, refreshFeatures } = 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 optionally icon, 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 channels override, 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:

  1. Org global settings β€” Developer can disable all email or all push notifications globally
  2. Event config β€” Per-event enabled/disabled and per-channel (email/push) toggles
  3. Workspace preferences β€” End-user overrides (only for events marked userManaged)
  4. 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)

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 β€” TranslationKey union type with autocomplete, catches typos at compile time
  • Native numerals β€” Hindi uses Devanagari digits, Arabic uses Arabic-Indic digits
  • RTL support β€” dir attribute 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/PluralRules instances 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.

πŸ‘€ User Management

User Attributes

Manage custom user attributes (key-value pairs):

import { useUserAttributes } from '@buildbase/sdk/react';

function UserProfile() {
  const { attributes, isLoading, updateAttribute, updateAttributes, refreshAttributes } =
    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, 'user@example.com', '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 cents as localized price string
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
getQuotaDisplayValue(quotaByInterval, interval?) Normalize IQuotaByInterval to { included, overage?, unitSize? }
formatQuotaWithPrice(value, unitName, options?) Format as "X included, then $Y.YY / unit"

Types: QuotaDisplayValue, FormatQuotaWithPriceOptions. Plan/subscription types use IQuotaByInterval and IQuotaIntervalValue for per-interval quotas and overages.

import {
  getCurrencySymbol,
  formatCents,
  getPricingVariant,
  getBasePriceCents,
  getQuotaDisplayValue,
  formatQuotaWithPrice,
} from '@buildbase/sdk/react';

// Display price for a plan version in a currency
const variant = getPricingVariant(planVersion, 'usd');
const cents = getBasePriceCents(planVersion, 'usd', 'monthly');
if (cents != null) {
  console.log(getCurrencySymbol('usd') + (cents / 100).toFixed(2));
}

// Quota display with overage
const display = getQuotaDisplayValue(planVersion.quotas?.videos, 'monthly');
const text = formatQuotaWithPrice(display, 'video', { currency: 'usd' });

πŸ“Š Quota Usage Tracking

Track and monitor metered usage for subscription quotas (e.g., API calls, emails, storage). Usage can be recorded from both the client-side (React app) and server-side (your backend).

When to use which?

Scenario Where to record Why
User clicks "Send Email" button Client-side (SDK hook) User-initiated, immediate UI feedback needed
API request hits your backend Server-side (REST API) Backend controls the resource, more secure
Background job processes data Server-side (REST API) No browser context available
File upload completes Either Depends on where validation happens

As a general rule: record usage where the resource is consumed. If your backend processes the work, record from the backend. If it's a client-side action, record from the client.


Client-Side (React SDK)

Use the SDK hooks inside your React app. Quota gate components (see Quota Gates) automatically refresh after recording.

Record Usage

import { useRecordUsage, useSaaSWorkspaces } from '@buildbase/sdk/react';

function SendEmailButton() {
  const { currentWorkspace } = useSaaSWorkspaces();
  const { recordUsage, loading, error } = useRecordUsage(currentWorkspace?._id);

  const handleSend = async () => {
    try {
      const result = await recordUsage({
        quotaSlug: 'emails',
        quantity: 1,
        source: 'web-app', // optional: track where usage came from
        idempotencyKey: 'email-abc', // optional: prevent duplicate recordings
      });
      console.log(`Used: ${result.consumed}/${result.included}, Available: ${result.available}`);
      if (result.overage > 0) {
        console.warn(`Overage: ${result.overage} units`);
      }
    } catch (err) {
      console.error('Failed to record usage:', err);
    }
  };

  return (
    <button onClick={handleSend} disabled={loading}>
      Send Email
    </button>
  );
}

Check Single Quota Status

import { useQuotaUsageStatus, useSaaSWorkspaces } from '@buildbase/sdk/react';

function QuotaStatusBar({ quotaSlug }: { quotaSlug: string }) {
  const { currentWorkspace } = useSaaSWorkspaces();
  const { status, loading, refetch } = useQuotaUsageStatus(currentWorkspace?._id, quotaSlug);

  if (loading) return <Spinner />;
  if (!status) return null;

  const usagePercent = Math.round((status.consumed / status.included) * 100);

  return (
    <div>
      <p>
        {quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)
      </p>
      <p>Available: {status.available}</p>
      {status.hasOverage && <p>Overage: {status.overage} units</p>}
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

Check All Quotas

import { useAllQuotaUsage, useSaaSWorkspaces } from '@buildbase/sdk/react';

function QuotaDashboard() {
  const { currentWorkspace } = useSaaSWorkspaces();
  const { quotas, loading, refetch } = useAllQuotaUsage(currentWorkspace?._id);

  if (loading) return <Spinner />;
  if (!quotas) return <p>No quota data available</p>;

  return (
    <div>
      {Object.entries(quotas).map(([slug, usage]) => (
        <div key={slug}>
          <strong>{slug}</strong>: {usage.consumed} / {usage.included}
          {usage.hasOverage && <span> (overage: {usage.overage})</span>}
        </div>
      ))}
    </div>
  );
}

Usage Logs

import { useUsageLogs, useSaaSWorkspaces } from '@buildbase/sdk/react';

function UsageLogsTable() {
  const { currentWorkspace } = useSaaSWorkspaces();
  const { logs, totalDocs, totalPages, page, hasNextPage, loading, refetch } = useUsageLogs(
    currentWorkspace?._id,
    'api_calls', // optional: filter by quota slug
    { limit: 20, page: 1 } // optional: pagination and filters
  );

  if (loading) return <Spinner />;

  return (
    <div>
      <table>
        <thead>
          <tr>
            <th>Quota</th>
            <th>Quantity</th>
            <th>Source</th>
            <th>Date</th>
          </tr>
        </thead>
        <tbody>
          {logs.map(log => (
            <tr key={log._id}>
              <td>{log.quotaSlug}</td>
              <td>{log.quantity}</td>
              <td>{log.source ?? '-'}</td>
              <td>{new Date(log.createdAt).toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <p>
        Page {page} of {totalPages} ({totalDocs} total)
      </p>
    </div>
  );
}

useUsageLogs parameters:

Parameter Type Required Description
workspaceId string | null | undefined Yes Workspace ID (null/undefined disables fetching)
quotaSlug string No Filter logs by quota slug
options.from string No ISO date string β€” filter logs from this date
options.to string No ISO date string β€” filter logs until this date
options.source string No Filter logs by source
options.page number No Page number (default: 1)
options.limit number No Results per page (default: 20)

Client-Side Hooks Summary

Hook Purpose
useRecordUsage(workspaceId) Record quota consumption (mutation)
useQuotaUsageStatus(workspaceId, quotaSlug) Get single quota status (auto-fetches)
useAllQuotaUsage(workspaceId) Get all quotas status (auto-fetches)
useUsageLogs(workspaceId, quotaSlug?, options?) Get paginated usage history (auto-fetches)

Server-Side (REST API)

For backend services, background jobs, or API routes β€” call the BuildBase API directly. This is the recommended approach when usage happens on your server (e.g., processing an API request, running a cron job, handling webhooks).

Step 1: Get a Session ID

Exchange your org API token for a session ID. You can do this once and reuse the session for multiple requests (default expiry: 30 days).

// Do this once at startup or cache the result
const TOKEN = 'your-org-id:your-api-secret'; // from BuildBase dashboard

async function getSessionId(): Promise<string> {
  const response = await fetch('https://your-server.buildbase.app/api/v1/public/token/exchange', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      token: TOKEN,
      expiresIn: 2592000, // 30 days (optional, default is 30 days)
    }),
  });
  const data = await response.json();
  return data.sessionId;
}

Step 2: Record Usage

const SESSION_ID = await getSessionId();
const BASE_URL = 'https://your-server.buildbase.app/api/v1/public';

async function recordUsage(workspaceId: string, quotaSlug: string, quantity: number) {
  const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-session-id': SESSION_ID,
    },
    body: JSON.stringify({
      quotaSlug,
      quantity,
      source: 'backend', // optional: helps distinguish from client-side usage
      metadata: {}, // optional: attach custom data
      idempotencyKey: undefined, // optional: prevent duplicate recordings
    }),
  });
  return response.json();
}

// Example: Record usage in an Express route handler
app.post('/api/generate-report', async (req, res) => {
  const { workspaceId } = req.user; // your auth

  // Record quota usage BEFORE or AFTER doing the work
  const usage = await recordUsage(workspaceId, 'reports', 1);

  if (usage.available <= 0 && !usage.hasOverage) {
    return res.status(429).json({ error: 'Report quota exceeded' });
  }

  // ... generate the report ...
  res.json({ success: true, quotaRemaining: usage.available });
});

Step 3: Check Usage Status (Optional)

// Get status for a single quota
async function getQuotaStatus(workspaceId: string, quotaSlug: string) {
  const response = await fetch(
    `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/status?quotaSlug=${quotaSlug}`,
    { headers: { 'x-session-id': SESSION_ID } }
  );
  return response.json();
  // Returns: { quotaSlug, consumed, included, available, overage, hasOverage }
}

// Get status for ALL quotas
async function getAllQuotaStatus(workspaceId: string) {
  const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`, {
    headers: { 'x-session-id': SESSION_ID },
  });
  return response.json();
  // Returns: { quotas: { [slug]: { consumed, included, available, overage, hasOverage } } }
}

// Example: Check before allowing an action
app.post('/api/send-email', async (req, res) => {
  const status = await getQuotaStatus(req.user.workspaceId, 'emails');

  if (status.available <= 0) {
    return res.status(429).json({
      error: 'Email quota exceeded',
      consumed: status.consumed,
      included: status.included,
    });
  }

  await recordUsage(req.user.workspaceId, 'emails', 1);
  // ... send the email ...
});

Batch Usage Recording (High-Volume)

For bulk operations (batch exports, cron jobs, webhooks) that process hundreds or thousands of items, use the batch endpoint to record all usage in a single request instead of calling the API per-item.

// Using BuildBase SDK (recommended)
import { BuildBase } from '@buildbase/sdk';

const bb = BuildBase({ serverUrl: BASE_URL, version: 'v1', orgId: ORG_ID, token: TOKEN });

await bb.usage.recordBatch(workspaceId, {
  items: [
    { quotaSlug: 'images', quantity: 500, source: 'batch-export-job' },
    { quotaSlug: 'videos', quantity: 10, source: 'batch-export-job' },
    { quotaSlug: 'images', quantity: 200, metadata: { jobId: 'abc123' } },
  ],
});
// Returns: { success, total: 3, succeeded: 3, failed: 0, results: [...] }
// Using REST API directly
const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/batch`, {
  method: 'POST',
  headers: { 'x-session-id': SESSION_ID, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [
      { quotaSlug: 'images', quantity: 500, source: 'batch-export' },
      { quotaSlug: 'videos', quantity: 10 },
    ],
  }),
});
  • Max 100 items per request
  • Each item is processed independently β€” a single failure doesn't fail the batch
  • Supports metadata, source, and idempotencyKey per item
  • Returns per-item results with success or error for each

Server-Side API Reference

Endpoint Method Description
/api/v1/public/token/exchange POST Exchange API token for session ID
/api/v1/public/workspaces/:id/subscription/usage POST Record quota usage
/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X GET Get single quota status
/api/v1/public/workspaces/:id/subscription/usage/all GET Get all quotas status
/api/v1/public/workspaces/:id/subscription/usage/logs GET Get paginated usage logs

All endpoints (except /token/exchange) require the x-session-id header.

Record usage request body:

Field Type Required Description
quotaSlug string Yes Quota identifier (e.g. 'api_calls', 'emails', 'storage')
quantity number Yes Units to consume (minimum 1)
metadata object No Custom metadata to attach to the usage record
source string No Source identifier (e.g. 'backend', 'worker', 'cron')
idempotencyKey string No Unique key for deduplication

Record usage response:

Field Type Description
used number Quantity recorded in this request
consumed number Total usage in the current billing period
included number Total units included in the plan
available number Remaining units before overage
overage number Units used beyond the included amount
billedAsync boolean Whether overage billing was queued to Stripe

🚦 Quota Gates

Control UI visibility based on quota usage status. Quota data is loaded once per workspace via QuotaUsageContextProvider (included in SaaSOSProvider by default) and refetched automatically after recording usage.

Gate Components

import {
  WhenQuotaAvailable,
  WhenQuotaExhausted,
  WhenQuotaOverage,
  WhenQuotaThreshold,
} from '@buildbase/sdk/react';

function Dashboard() {
  return (
    <div>
      {/* Show action button only when quota has remaining units */}
      <WhenQuotaAvailable slug="api_calls">
        <MakeApiCallButton />
      </WhenQuotaAvailable>

      {/* Show upgrade prompt when quota is fully consumed */}
      <WhenQuotaExhausted slug="api_calls">
        <UpgradePrompt message="You've used all your API calls this month." />
      </WhenQuotaExhausted>

      {/* Show warning when usage exceeds included amount (overage billing active) */}
      <WhenQuotaOverage slug="api_calls">
        <OverageBillingWarning />
      </WhenQuotaOverage>

      {/* Show warning when usage reaches 80% of included amount */}
      <WhenQuotaThreshold slug="api_calls" threshold={80}>
        <p>Warning: You've used over 80% of your API calls.</p>
      </WhenQuotaThreshold>
    </div>
  );
}

With Loading and Fallback

All quota gate components support optional loadingComponent and fallbackComponent props:

<WhenQuotaAvailable
  slug="emails"
  loadingComponent={<Skeleton className="h-10" />}
  fallbackComponent={<p>Email quota exhausted. <a href="/upgrade">Upgrade now</a></p>}
>
  <SendEmailButton />
</WhenQuotaAvailable>

<WhenQuotaThreshold
  slug="storage"
  threshold={90}
  loadingComponent={<Spinner />}
  fallbackComponent={null}
>
  <StorageWarningBanner />
</WhenQuotaThreshold>

Quota Gate Components Reference

Component Renders children when Props
WhenQuotaAvailable Quota has remaining units (available > 0) slug, children, loadingComponent?, fallbackComponent?
WhenQuotaExhausted Quota is fully consumed (available <= 0) slug, children, loadingComponent?, fallbackComponent?
WhenQuotaOverage Usage exceeds included amount (hasOverage) slug, children, loadingComponent?, fallbackComponent?
WhenQuotaThreshold Usage percentage >= threshold slug, threshold (0-100), children, loadingComponent?, fallbackComponent?

All gates must be used inside QuotaUsageContextProvider (included in SaaSOSProvider). By default they return null while loading or when the condition is not met.

useQuotaUsageContext

Use the hook when you need raw quota data or a manual refetch (e.g. after a bulk operation):

import { useQuotaUsageContext } from '@buildbase/sdk/react';

function QuotaDebug() {
  const { quotas, loading, refetch } = useQuotaUsageContext();

  if (loading) return <Spinner />;
  if (!quotas) return <p>No quota data</p>;

  return (
    <div>
      {Object.entries(quotas).map(([slug, usage]) => (
        <p key={slug}>
          {slug}: {usage.consumed}/{usage.included}
        </p>
      ))}
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  );
}
Property Type Description
quotas Record<string, IQuotaUsageStatus> | null Current quota usage data keyed by slug
loading boolean True while quota data is being fetched
refetch () => Promise<void> Manually refetch all quota usage

When quota usage refetches:

  • When the current workspace changes (automatic).
  • When usage is recorded via useRecordUsage β€” refetch is triggered automatically.
  • When you call refetch() manually.

πŸ“ Beta Form Component

Use the pre-built BetaForm component for signup/waitlist forms:

import { BetaForm } from '@buildbase/sdk/react';

function SignupPage() {
  return (
    <BetaForm
      onSuccess={() => console.log('Form submitted!')}
      onError={error => console.error(error)}
      showSuccessMessage={true}
      hideLogo={false}
      hideTitles={false}
    />
  );
}

πŸ“‘ Event System

Subscribe to SDK events for user and workspace changes:

import { SaaSOSProvider } from '@buildbase/sdk/react';
import { eventEmitter } from '@buildbase/sdk';

// In your provider configuration
<SaaSOSProvider
  auth={{
    callbacks: {
      handleEvent: async (eventType, data) => {
        switch (eventType) {
          case 'user:created':
            console.log('User created:', data.user);
            break;
          case 'workspace:changed':
            console.log('Workspace changed:', data.workspace);
            break;
          case 'workspace:user-added':
            console.log('User added to workspace:', data.userId);
            break;
          // ... handle other events
        }
      },
    },
  }}
>
  {children}
</SaaSOSProvider>;

Available Events

  • user:created - User account created
  • user:updated - User profile updated
  • workspace:changed - Workspace switched (fires after switch completes; use onWorkspaceChange for prep before switch)
  • workspace:created - New workspace created
  • workspace:updated - Workspace updated
  • workspace:deleted - Workspace deleted
  • workspace:user-added - User added to workspace
  • workspace:user-removed - User removed from workspace
  • workspace:user-role-changed - User role changed

πŸ›‘οΈ Error Handling

The SDK handles errors internally: API failures, auth errors, and component errors are logged and surfaced through hook states (e.g. error from useSaaSWorkspaces) and callbacks. SaaSOSProvider wraps its children in an internal SDKErrorBoundary to catch React render errors inside the SDK tree. For app-level errors, wrap your app (or routes) in your own error boundary (e.g. React’s ErrorBoundary or your framework’s error UI). For failed async operations, check the error property on hooks and show user feedback (e.g. toast or inline message). See Error codes for SDK error codes and HTTP mappings.

βš™οΈ Settings

Access OS-level settings:

import { useSaaSSettings } from '@buildbase/sdk/react';

function SettingsExample() {
  const { settings, getSettings } = useSaaSSettings();

  return (
    <div>
      <p>Max Workspaces: {settings?.workspace.maxWorkspaces}</p>
    </div>
  );
}

πŸ“š API Reference

Central APIs

All SDK API clients extend a shared base class and are exported from the package:

Export Purpose
BaseApi Abstract base (URL, auth, fetchJson/fetchResponse) – extend for custom APIs
IBaseApiConfig Config type: serverUrl, version, optional orgId
UserApi User attributes and features
WorkspaceApi Workspaces, subscription, invoices, quota usage, users
SettingsApi Organization settings

Components

Component Purpose
SubscriptionContextProvider Provides subscription data to children (included in SaaSOSProvider)
WhenSubscription, WhenNoSubscription, WhenSubscriptionToPlans Subscription gate components
WhenTrialing, WhenNotTrialing, WhenTrialEnding Trial gate components
WhenQuotaAvailable, WhenQuotaExhausted, WhenQuotaOverage Quota gate components

Currency, pricing variant & quota utilities

Category Exports
Currency CURRENCY_DISPLAY, CURRENCY_FLAG, PLAN_CURRENCY_CODES, PLAN_CURRENCY_OPTIONS, getCurrencySymbol, getCurrencyFlag, formatCents, formatOverageRate, formatOverageRateWithLabel, formatQuotaIncludedOverage, getQuotaUnitLabelFromName
Pricing variants getPricingVariant, getBasePriceCents, getStripePriceIdForInterval, getQuotaOverageCents, getQuotaDisplayWithVariant, getAvailableCurrenciesFromPlans, getDisplayCurrency, getBillingIntervalAndCurrencyFromPriceId; types: IPricingVariant, PlanVersionWithPricingVariants, QuotaDisplayWithOverage
Quota getQuotaDisplayValue, formatQuotaWithPrice; types: QuotaDisplayValue, FormatQuotaWithPriceOptions. Plan types use IQuotaByInterval, IQuotaIntervalValue for per-interval quotas.

Get OS config from useSaaSOs() and instantiate API classes when you need low-level access; otherwise prefer the high-level hooks (useSaaSWorkspaces, useUserAttributes, useSaaSSettings, etc.):

import { useSaaSOs } from '@buildbase/sdk/react';
import { UserApi, WorkspaceApi, SettingsApi } from '@buildbase/sdk';

const os = useSaaSOs();
const workspaceApi = new WorkspaceApi({
  serverUrl: os.serverUrl,
  version: os.version,
  orgId: os.orgId,
});
// Similarly: new UserApi({ ... }), new SettingsApi({ ... })

Hooks

Prefer these SDK hooks for state and operations instead of useAppSelector:

Hook Purpose
useSaaSAuth() Auth state (user, session, status), signIn(returnUrl?), signOut, openWorkspaceSettings
useSaaSWorkspaces() Workspaces, currentWorkspace, loading, switching/switchingToId, CRUD and switch actions
useSaaSOs() OS config (serverUrl, version, orgId, auth, settings) when you need the full config object
useSaaSSettings() Organization settings and getSettings (prefer this when you only need settings)
useUserAttributes() User attributes and update/refresh
useUserFeatures() User feature flags
useSubscriptionContext() Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider
useTrialStatus() Trial state: isTrialing, daysRemaining, trialEndsAt, isTrialEnding
usePushNotifications() Push notification state and actions: isSubscribed, subscribe(), unsubscribe()
Subscription hooks usePublicPlans, useSubscription, useSubscriptionManagement, usePlanGroup, usePlanGroupVersions, usePublicPlanGroupVersion, useCreateCheckoutSession, useUpdateSubscription, useCancelSubscription, useResumeSubscription, useInvoices, useInvoice
useQuotaUsageContext() Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider
Quota usage hooks useRecordUsage, useQuotaUsageStatus, useAllQuotaUsage, useUsageLogs
Invalidation helpers invalidateSubscription(), invalidateQuotaUsage() β€” trigger context refetch after server-side mutations

Using hooks keeps your code stable if internal state shape changes and avoids direct Redux/context coupling.

Enums

  • ApiVersion - API version enum (currently only V1)
  • AuthStatus - Auth status enum: loading | redirecting | authenticating | authenticated | unauthenticated. Use with useSaaSAuth().status; isLoading, isAuthenticated, and isRedirecting are derived from it.

Types

All TypeScript types are exported for type safety. See the TypeScript definitions for complete type information.

Further documentation

  • Architecture – Layers, providers, APIs (BaseApi, UserApi, WorkspaceApi, SettingsApi), state, auth flow
  • Error codes – SDK error codes and HTTP status mappings

βš™οΈ Configuration Reference

SaaSOSProvider Props

Prop Type Required Description
serverUrl string βœ… API server URL (must be valid URL)
version ApiVersion βœ… API version (currently only 'v1')
orgId string βœ… Organization ID (must be valid MongoDB ObjectId - 24 hex characters)
auth IAuthConfig ❌ Authentication configuration
locale SDKLocale ❌ SDK UI language ('en', 'es', 'fr', 'de', 'ja', 'zh', 'hi', 'ar')
defaultPermissions Record<string, string[]> ❌ Default app permissions per role
getCheckoutStripeParams GetCheckoutStripeParams ❌ Async callback called before every Stripe checkout to return metadata, referral IDs, etc.
children ReactNode βœ… React children

Auth Configuration

interface IAuthConfig {
  clientId: string; // OAuth client ID
  redirectUrl: string; // OAuth redirect URL
  callbacks: {
    // Required: restore session on page refresh (reads httpOnly cookie via server endpoint)
    getSession: () => Promise<string | null>;
    // Required: exchange OAuth code for sessionId (sets httpOnly cookie on server)
    handleAuthentication: (code: string) => Promise<{ sessionId: string }>;
    // Required: clear session on sign out (clears httpOnly cookie on server)
    onSignOut: () => Promise<void>;
    // Optional: listen to SDK events
    handleEvent?: (eventType: EventType, data: EventData) => void | Promise<void>;
    // Optional: called before workspace switch
    onWorkspaceChange?: (params: OnWorkspaceChangeParams) => Promise<void>;
  };
}

interface OnWorkspaceChangeParams {
  workspace: IWorkspace;
  user: AuthUser | null;
  role: string | null;
}

Session flow (same pattern as next-auth):

  • Session token is stored in an httpOnly cookie (set by your server, not readable by JS)
  • On page refresh, the SDK calls getSession() once to restore the session
  • Session data (user info) lives in-memory only (React context) β€” no localStorage

Validation Requirements

  • serverUrl: Must be a valid URL (e.g., https://api.example.com)
  • version: Must be exactly 'v1' (only supported version)
  • orgId: Must be a valid MongoDB ObjectId (24 hexadecimal characters, e.g., 507f1f77bcf86cd799439011)

BetaForm Props

Prop Type Default Description
onSuccess () => void - Callback when form submits successfully
onError (error: string) => void - Callback when form submission fails
className string 'w-full' CSS class for form container
fieldClassName string 'flex flex-col gap-1.5 w-full' CSS class for form fields
autoFocus boolean true Auto-focus name field
showSuccessMessage boolean true Show success message after submit
hideLogo boolean false Hide logo
hideTitles boolean false Hide titles

Note: The form language is inherited from the SaaSOSProvider locale setting. Supported locales: en, es, fr, de, ja, zh, hi, ar.

🎯 Common Patterns

Pattern 1: Protected Routes

import { WhenAuthenticated, WhenUnauthenticated } from '@buildbase/sdk/react';

function App() {
  return (
    <WhenUnauthenticated>
      <LoginPage />
    </WhenUnauthenticated>

    <WhenAuthenticated>
      <ProtectedRoutes />
    </WhenAuthenticated>
  );
}

function ProtectedRoutes() {
  return (
    <Routes>
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/settings" element={<Settings />} />
    </Routes>
  );
}

Pattern 2: Role-Based Navigation

import { WhenRoles } from '@buildbase/sdk/react';

function Navigation() {
  return (
    <nav>
      <Link to="/dashboard">Dashboard</Link>
      <Link to="/projects">Projects</Link>

      <WhenRoles roles={['admin', 'owner']}>
        <Link to="/admin">Admin Panel</Link>
      </WhenRoles>

      <WhenRoles roles={['admin']}>
        <Link to="/settings">Settings</Link>
      </WhenRoles>
    </nav>
  );
}

Pattern 3: Workspace Context Provider

import { useSaaSWorkspaces } from '@buildbase/sdk/react';
import { createContext, useContext } from 'react';

const WorkspaceContext = createContext(null);

export function WorkspaceProvider({ children }) {
  const workspaceData = useSaaSWorkspaces();

  return <WorkspaceContext.Provider value={workspaceData}>{children}</WorkspaceContext.Provider>;
}

export function useWorkspace() {
  return useContext(WorkspaceContext);
}

Pattern 4: Feature Gated Components

import { WhenWorkspaceFeatureEnabled } from '@buildbase/sdk/react';

function Dashboard() {
  return (
    <div>
      <StandardFeatures />

      <WhenWorkspaceFeatureEnabled slug="advanced-analytics">
        <AdvancedAnalytics />
      </WhenWorkspaceFeatureEnabled>

      <WhenWorkspaceFeatureEnabled slug="ai-assistant">
        <AIAssistant />
      </WhenWorkspaceFeatureEnabled>
    </div>
  );
}

Pattern 4b: Subscription-Gated UI

import {
  WhenSubscription,
  WhenNoSubscription,
  WhenSubscriptionToPlans,
} from '@buildbase/sdk/react';

function BillingPage() {
  return (
    <div>
      <WhenNoSubscription>
        <UpgradePrompt />
      </WhenNoSubscription>

      <WhenSubscription>
        <BillingSettings />
      </WhenSubscription>

      <WhenSubscriptionToPlans plans={['pro', 'enterprise']}>
        <AdvancedBillingFeatures />
      </WhenSubscriptionToPlans>
    </div>
  );
}

SubscriptionContextProvider is included in SaaSOSProvider by default, so no extra wrapper is needed.

Pattern 4c: Quota-Gated UI

import { WhenQuotaAvailable, WhenQuotaExhausted, WhenQuotaThreshold } from '@buildbase/sdk/react';

function FeatureWithQuota() {
  return (
    <div>
      <WhenQuotaThreshold slug="api_calls" threshold={80}>
        <WarningBanner message="You're running low on API calls" />
      </WhenQuotaThreshold>

      <WhenQuotaAvailable slug="api_calls" fallbackComponent={<UpgradePrompt />}>
        <ApiCallButton />
      </WhenQuotaAvailable>

      <WhenQuotaExhausted slug="api_calls">
        <p>
          No API calls remaining. <a href="/billing">Upgrade your plan</a>
        </p>
      </WhenQuotaExhausted>
    </div>
  );
}

QuotaUsageContextProvider is included in SaaSOSProvider by default, so no extra wrapper is needed. Quota data auto-refreshes after useRecordUsage calls.

Pattern 5: Handling Workspace Changes

import { useSaaSWorkspaces } from '@buildbase/sdk/react';
import { useEffect } from 'react';

function App() {
  const { currentWorkspace, switching } = useSaaSWorkspaces();

  useEffect(() => {
    if (currentWorkspace) {
      // Update your app state when workspace changes
      console.log('Workspace changed:', currentWorkspace);
      // Reload data, update context, etc.
    }
  }, [currentWorkspace]);

  // Show loading during switch; use switchingToId for the workspace ID being switched to
  if (switching) return <LoadingOverlay />;

  return <YourApp />;
}

For token generation or prep before switching, configure onWorkspaceChange in auth callbacks (see Quick Start)β€”it receives { workspace, user, role }.

Pattern 6: Error Boundary

Wrap your app (or a subtree) with an error boundary to catch React errors and show a fallback. Use your framework’s boundary (e.g. React’s ErrorBoundary or Next.js error UI). In catch blocks for async operations, show user feedback (e.g. toast or inline message) using the error state from hooks.

πŸ”§ Troubleshooting

Common Issues

1. "Invalid orgId" Error

Problem: orgId must be a valid MongoDB ObjectId (24 hexadecimal characters).

Solution:

// ❌ Wrong
orgId = '123';

// βœ… Correct
orgId = '507f1f77bcf86cd799439011'; // 24 hex characters

2. "Invalid serverUrl" Error

Problem: serverUrl must be a valid URL.

Solution:

// ❌ Wrong
serverUrl = 'api.example.com';
serverUrl = 'not-a-url';

// βœ… Correct
serverUrl = 'https://api.example.com';
serverUrl = 'http://localhost:3000';

3. Authentication Not Working

Problem: User can't sign in or session not persisting.

Solutions:

  • Ensure handleAuthentication callback returns { sessionId: string }
  • Check that your backend API is correctly exchanging the OAuth code
  • Verify redirectUrl matches your OAuth app configuration
  • Check browser console for error messages
// βœ… Correct callback
handleAuthentication: async (code: string) => {
  const response = await fetch('/api/auth/token', {
    method: 'POST',
    body: JSON.stringify({ code }),
  });
  const data = await response.json();
  return { sessionId: data.sessionId }; // Must return sessionId
};

4. Workspace Not Loading

Problem: Workspaces array is empty or not updating.

Solutions:

  • Ensure user is authenticated before fetching workspaces
  • Call fetchWorkspaces() explicitly if needed
  • Check network tab for API errors
  • Verify user has access to workspaces
const { fetchWorkspaces, workspaces } = useSaaSWorkspaces();

useEffect(() => {
  if (isAuthenticated) {
    fetchWorkspaces(); // Explicitly fetch if needed
  }
}, [isAuthenticated]);

5. Feature Flags Not Working

Problem: Feature flags always return false or undefined.

Solutions:

  • Ensure getFeatures() is called (usually automatic)
  • Check that feature slugs match your backend configuration
  • Verify workspace has the feature enabled
  • Check user has the feature enabled (for user-level features)
const { getFeatures, currentWorkspace } = useSaaSWorkspaces();

useEffect(() => {
  getFeatures(); // Ensure features are loaded
}, []);

6. TypeScript Errors

Problem: Type errors when using the SDK.

Solutions:

  • Ensure you're using React 19+ (check peer dependencies)

  • Import types explicitly if needed:

    import type { IWorkspace, IUser } from '@buildbase/sdk';
  • Check that all required props are provided

7. CSS Not Loading

Problem: Components look unstyled.

Solution: Ensure CSS is imported:

import '@buildbase/sdk/css';

FAQ

Q: Can I use this with Next.js?
A: Yes! Just ensure you use 'use client' directive in components using SDK hooks.

Q: Can I use this with other React frameworks?
A: Yes, as long as you're using React 19+.

Q: How do I customize the workspace switcher UI?
A: Use the trigger render prop to fully customize the UI.

Q: Can I use multiple workspaces simultaneously?
A: No, the SDK manages one current workspace at a time. Use switchToWorkspace() (runs onWorkspaceChange first) or setCurrentWorkspace() (direct set, bypasses callback).

Q: How do I handle offline scenarios?
A: The session token is stored in an httpOnly cookie (set by your server). On page refresh, the SDK calls your getSession callback to restore it. Handle offline scenarios in your handleAuthentication and getSession callbacks.

Q: Can I use this without TypeScript?
A: Yes, but TypeScript is recommended for better developer experience.

πŸ’‘ Best Practices

1. Provider Setup

βœ… Do: Wrap your entire app with SaaSOSProvider at the root level.

// βœ… Good
function App() {
  return (
    <SaaSOSProvider {...config}>
      <YourApp />
    </SaaSOSProvider>
  );
}

❌ Don't: Nest providers or use multiple instances.

// ❌ Bad
<SaaSOSProvider>
  <SaaSOSProvider>
    {' '}
    {/* Don't nest */}
    <App />
  </SaaSOSProvider>
</SaaSOSProvider>

2. Error Handling

βœ… Do: Wrap your app with an error boundary (your framework’s or React’s) to catch render errors.

βœ… Do: Handle async failures using the error from hooks and show user feedback (e.g. toast or inline message).

3. State Access: Prefer SDK Hooks Over useAppSelector

βœ… Do: Use SDK hooks for auth, workspace, and OS state.

// βœ… Good – use hooks
const { user, isAuthenticated } = useSaaSAuth();
const { workspaces, currentWorkspace, switchingToId } = useSaaSWorkspaces();
const { settings } = useSaaSSettings();
const os = useSaaSOs(); // when you need full OS config (serverUrl, version, orgId, etc.)

❌ Don't: Use useAppSelector(state => state.auth) or similar in app codeβ€”prefer the hooks above so the SDK can evolve internal state without breaking you.

4. Workspace Management

βœ… Do: Use useSaaSWorkspaces hook for workspace operations.

// βœ… Good
const { currentWorkspace, switchToWorkspace, switching, switchingToId } = useSaaSWorkspaces();
// switchToWorkspace: runs onWorkspaceChange first (token gen, etc.)
// switching: true when switch is in progress; switchingToId: workspace ID being switched to

βœ… Do: Configure onWorkspaceChange in auth callbacks for token generationβ€”receives { workspace, user, role }.

❌ Don't: Manually manage workspace state.

// ❌ Bad
const [workspace, setWorkspace] = useState(null); // Don't do this

5. Feature Flags

βœ… Do: Use feature flag components for conditional rendering.

// βœ… Good
<WhenWorkspaceFeatureEnabled slug="feature">
  <FeatureComponent />
</WhenWorkspaceFeatureEnabled>

βœ… Do: Check features programmatically when needed.

// βœ… Good
const { isFeatureEnabled } = useUserFeatures();
if (isFeatureEnabled('premium')) {
  // Do something
}

6. Authentication

βœ… Do: Use WhenAuthenticated/WhenUnauthenticated for route protection.

// βœ… Good
<WhenAuthenticated>
  <ProtectedRoute />
</WhenAuthenticated>

βœ… Do: Handle authentication errors gracefully.

// βœ… Good
const { signIn, status } = useSaaSAuth();
<button onClick={() => signIn()} disabled={status === 'loading'}>
  {status === 'loading' ? 'Signing in...' : 'Sign In'}
</button>;

7. Event Handling

βœ… Do: Handle events in your provider configuration. Use onWorkspaceChange for prep before switch (e.g. generate token), and handleEvent for post-switch notifications.

// βœ… Good
<SaaSOSProvider
  auth={{
    callbacks: {
      onWorkspaceChange: async ({ workspace, user, role }) => {
        await generateTokenForWorkspace(workspace._id, user?.id, role);
      },
      handleEvent: async (eventType, data) => {
        if (eventType === 'workspace:changed') {
          // Workspace already switched; update app state
        }
      },
    },
  }}
>

8. TypeScript

βœ… Do: Use TypeScript for better type safety.

// βœ… Good
import type { IWorkspace, IUser } from '@buildbase/sdk';

function MyComponent({ workspace }: { workspace: IWorkspace }) {
  // Type-safe code
}

9. Performance

βœ… Do: Memoize expensive computations.

// βœ… Good
const filteredWorkspaces = useMemo(() => workspaces.filter(w => w.active), [workspaces]);

βœ… Do: Use refreshWorkspaces() for background updates instead of fetchWorkspaces().

// βœ… Good - doesn't block UI
refreshWorkspaces();

// Use fetchWorkspaces() only when you need to wait for the result
await fetchWorkspaces();

Server-Side Usage

The SDK also works on the server β€” API routes, background jobs, webhooks, cron tasks. Zero React dependency.

@buildbase/sdk        Server: BuildBase() factory, API classes, types, utilities
@buildbase/sdk/react  Client: React hooks, providers, gate components (documented above)

Setup (Next.js)

Configure once, use everywhere. Same pattern as Auth.js.

// lib/buildbase.ts
import BuildBase from '@buildbase/sdk';
import { cookies } from 'next/headers';

export const {
  auth,
  workspace,
  subscription,
  usage,
  plans,
  invoices,
  users,
  features,
  settings,
  notification,
  permissions,
  withSession,
  client,
} = BuildBase({
  serverUrl: process.env.BUILDBASE_URL!,
  orgId: process.env.BUILDBASE_ORG_ID!,
  getSessionId: async () => {
    const c = await cookies();
    return c.get('bb-session-id')?.value ?? null;
  },
});

Use in API routes β€” session is resolved automatically:

// app/api/workspace/route.ts
import { auth, workspace, subscription } from '@/lib/buildbase';

export async function GET() {
  const session = await auth();
  if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });

  const workspaces = await workspace.list();
  return Response.json({ workspaces });
}

Setup (Express)

No getSessionId callback β€” use withSession() per-request instead.

// buildbase.ts
import BuildBase from '@buildbase/sdk';

const bb = BuildBase({
  serverUrl: process.env.BUILDBASE_URL!,
  orgId: process.env.BUILDBASE_ORG_ID!,
});

export const { withSession, plans } = bb;
// routes
app.get('/workspaces', async (req, res) => {
  const { workspace } = withSession(req.headers['x-session-id']);
  res.json(await workspace.list());
});

app.post('/usage', async (req, res) => {
  const { usage } = withSession(req.headers['x-session-id']);
  const result = await usage.record(req.body.workspaceId, {
    quotaSlug: 'api-calls',
    quantity: 1,
  });
  res.json(result);
});

Background Jobs / Webhooks

For service accounts (no user session), use withSession() with a service token:

import { withSession } from '@/lib/buildbase';

const bb = withSession(process.env.SERVICE_SESSION_ID!);

// Record usage from a webhook
await bb.usage.record(workspaceId, {
  quotaSlug: 'uploads',
  quantity: 1,
  source: 'webhook:file.processed',
});

// Check subscription
const sub = await bb.subscription.get(workspaceId);

Server-Side Actions Reference

Module Methods
workspace list, get, create, update, delete
users list, invite, remove, updateRole, getProfile, updateProfile
subscription get, checkout, update, cancel, resume, getBillingPortalUrl
plans getGroup, getVersions, getPublic, getVersion
invoices list, get
usage record, recordBatch, getQuota, getAll, getLogs
settings get
features list, update
notification send(workspaceId, event, userId?, data?)
permissions check(workspaceId, userId, permission), resolve(workspaceId, userId)

BuildBase Config Options

BuildBase({
  serverUrl: '...',           // Required
  orgId: '...',               // Required
  getSessionId: async () => ..., // Session resolver (Next.js: read cookie)
  timeout: 30_000,            // Request timeout in ms (default: 30s)
  maxRetries: 2,              // Retry on network errors / 5xx
  debug: true,                // Log all API calls to console
  headers: { 'X-Source': 'cron' }, // Custom headers on every request
  onError: (err, ctx) => {    // Centralized error callback
    Sentry.captureException(err, { extra: ctx })
  },
  fetch: customFetch,         // Replace global fetch (testing, proxying)
})

🀝 Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ†˜ Support


Made with ❀️ by the BuildBase team