BuildBaseBuildBase

Overview

Add subscription plans, checkout, trials, and seat-based pricing to your app.

BuildBase handles subscription billing end-to-end. Use the headless PricingPage component for checkout, and openWorkspaceSettings('subscription') for management. Everything else - Stripe integration, plan switching, trials, invoices - is handled automatically.

Warning

Billing requires setup in the BuildBase dashboard before the SDK components will work:

  1. Connect Stripe - Go to Billing → Credentials and enter your Stripe API keys (use test keys for development)
  2. Create at least one plan - Go to Billing → Plans, create a plan with a name, price, and billing interval
  3. Publish a plan version - Plans use versioning. Create and publish at least one version so it appears on the pricing page
  4. Set up a pricing group - Go to Billing → Pricing groups. Group your plans so PricingPage knows what to display

Without these steps, PricingPage renders empty plans and selectPlan() fails.

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

function Pricing() {
  return (
    <PricingPage slug="main-pricing">
      {({ loading, error, plans, selectPlan }) => {
        if (loading) return <p>Loading plans...</p>;
        if (error) return <p>{error}</p>;

        return (
          <div className="grid grid-cols-3 gap-4">
            {plans.map((plan) => (
              <div key={plan._id}>
                <h3>{plan.name}</h3>
                <button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
                  Choose {plan.name}
                </button>
              </div>
            ))}
          </div>
        );
      }}
    </PricingPage>
  );
}
import { useSaaSAuth } from '@buildbase/sdk/react';

function ManageBilling() {
  const { openWorkspaceSettings } = useSaaSAuth();
  return (
    <button onClick={() => openWorkspaceSettings('subscription')}>
      Manage Subscription
    </button>
  );
}

Before you start

Create plans and pricing groups in the BuildBase dashboard first.

PricingPage

PricingPage is headless: it fetches plan data and handles checkout logic, and you render the UI through a render prop. Both slug (the pricing group to display) and children (the render function) are required.

The render prop receives:

  • plans - published plan versions with pricing, features, limits, and quotas
  • items - subscription items (features, limits, quotas) for building comparison tables
  • loading / error - fetch state
  • selectPlan(planVersionId, interval, currency) - opens the plan dialog for authenticated users, or triggers sign-in first (set redirectBaseUrl on PricingPage for the unauthenticated flow)
  • refetch() - refetch plan data

Optional props: loadingFallback (custom loading UI), errorFallback (custom error UI), and redirectBaseUrl (post-login redirect base for unauthenticated visitors).

The email on the Stripe checkout page is filled in for you and cannot be edited there. It is the workspace owner's address, so a teammate with billing rights checks out under the owner's email rather than their own. Your app never passes an email: selectPlan takes no email argument and the checkout endpoint rejects one.

openPlanPicker()

Opens the plan selection UI from anywhere in your app - perfect for "Upgrade" buttons inside feature gates or trial banners:

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

function UpgradeButton() {
  const { openPlanPicker } = useSaaSAuth();
  return <button onClick={() => openPlanPicker()}>Upgrade Plan</button>;
}

When your plans are priced in more than one currency and the workspace has no billing currency yet, the plan picker opens on the visitor's currency: the one for the country their browser's language settings name (en-GB is GBP, a bare de is Germany, so EUR). If your plans are not priced in it, it opens on USD, and without USD on the first currency offered. A checkout link's currency and the current subscription's currency come first. Nothing leaves the browser for this: there is no IP lookup and no location prompt, and the visitor can still pick another currency. The credit packages dialog does the same. From SDK 0.0.88; earlier versions open on the first currency in alphabetical order.

openWorkspaceSettings('subscription')

Opens the full subscription management screen where users can:

  • View current plan and billing interval
  • Upgrade or downgrade
  • Cancel or resume
  • View invoices and payment history
  • Access the Stripe billing portal

Changing plan

A plan change from the plan picker or useUpdateSubscription() is applied to the workspace's existing subscription. What happens next depends on the subscription:

  • During a trial, the plan changes and the trial keeps running: it ends when it would have, and nothing is charged at the change. The first charge is the new plan's price, when the trial ends. A trial started without a card still needs one before then, or Stripe cancels the subscription at the end of the trial.
  • An upgrade invoices the difference at once. When the card needs 3-D Secure or is declined, Stripe holds the upgrade until that invoice is paid, and the workspace stays on its current plan meanwhile. Plan & Billing shows a Complete payment button that opens Stripe's invoice page (from SDK 0.0.86; earlier versions say the plan was updated). If the invoice is not paid within 23 hours, Stripe drops the change.
  • A downgrade is not credited back. The workspace keeps its current plan's features until the end of the period it paid for.

The subscription.upgraded webhook fires when the upgrade is applied: at the change, or once its payment goes through.

Calling the hook yourself, check for a payment the user still has to make:

const result = await updateSubscription(planVersionId);

if ('checkoutUrl' in result) {
  window.location.href = result.checkoutUrl; // no subscription yet: a first checkout
} else if (result.paymentRequired && result.paymentUrl) {
  window.open(result.paymentUrl, '_blank', 'noopener,noreferrer'); // the upgrade waits on this payment
}

paymentRequired and paymentUrl are declared on ISubscriptionUpdateResponse from SDK 0.0.86; the server sends them to every version.

Reading subscription state

Use hooks when you need to display subscription info or conditionally render UI:

import {
  useSubscription,
  useTrialStatus,
  WhenNoSubscription,
  WhenSubscriptionToPlans,
  WhenTrialEnding,
} from '@buildbase/sdk/react';

function Dashboard({ workspaceId }) {
  const { subscription } = useSubscription(workspaceId);
  const { isTrialing, daysRemaining } = useTrialStatus();

  return (
    <>
      {subscription && <p>Plan: {subscription.plan?.name}</p>}
      {isTrialing && <p>{daysRemaining} days left in trial</p>}

      <WhenNoSubscription>
        <p>
          No plan yet. <a href="/pricing">Choose a plan</a>
        </p>
      </WhenNoSubscription>

      <WhenSubscriptionToPlans plans={['starter']}>
        <p>Upgrade to Pro for more features.</p>
      </WhenSubscriptionToPlans>

      <WhenTrialEnding daysThreshold={3}>
        <p>Trial ends soon - add a payment method to keep access.</p>
      </WhenTrialEnding>
    </>
  );
}

Hooks

HookWhat it returns
useSubscription(workspaceId)Current plan, status, billing interval
useTrialStatus()isTrialing, daysRemaining
useSeatStatus(workspace)memberCount, includedSeats, canInvite, inviteBlockReason

Conditional components

ComponentRenders when
WhenSubscriptionHas an active subscription
WhenNoSubscriptionNo subscription
WhenSubscriptionToPlansOn a specific plan (pass plans prop)
WhenTrialingIn trial period
WhenNotTrialingNot in trial
WhenTrialEndingTrial ends within daysThreshold days

Server-side

Check subscription status from your backend:

import BuildBase from '@buildbase/sdk';

const bb = BuildBase({ serverUrl, orgId, getSessionId });

const { subscription, plan } = await bb.subscription.get(workspaceId);

if (!subscription || subscription.subscriptionStatus !== 'active') {
  return Response.json({ error: 'Subscription required' }, { status: 403 });
}

Next Steps