BuildBaseBuildBase

Quickstart

Sign-in, a workspace switcher and billing settings in a Next.js App Router app, with every file shown.

This page adds BuildBase sign-in, a workspace switcher and the billing screen to a Next.js App Router app. npx buildbase init writes the wiring for you. If you would rather write it yourself, every file is shown below.

Before you start

An organization in the BuildBase console, its 24-character Organization ID (Settings > General), and an auth client with its client ID and client secret (User management > Access > Authentication). The secret is shown once, when you create the client.

Fastest: let buildbase init write it

Run it in the root of your app:

npm install @buildbase/sdk
npx buildbase init --org-id <your 24-character org id>

On a Next.js App Router app it writes these files, under src/ if your app uses it. A file that exists already is kept, not overwritten; the layout and env files are edited in place.

FileWhat it does
components/saas-provider.tsxThe provider, as a client component
app/layout.tsxYour root layout, edited to wrap {children} in the provider (when it cannot, init prints the lines to add)
app/api/auth/token/route.tsSwaps the sign-in code for a session, kept in an httpOnly cookie
app/api/auth/session/route.tsTells the provider who is signed in after a refresh
app/api/auth/signout/route.tsClears the session cookie
app/api/webhooks/buildbase/route.tsVerifies and handles webhooks
lib/buildbase.tsThe server-side client for route handlers and server components
.env.example, .env.localThe environment keys, with secrets left empty (.env.local only when your .gitignore covers it)

Then fill in NEXT_PUBLIC_BUILDBASE_CLIENT_ID (or pass --client-id) and BUILDBASE_CLIENT_SECRET in .env.local, and add http://localhost:3000, init's default redirect URL, to your client's redirect URLs in the console. Until the client ID is set, the generated provider renders your app without BuildBase, and the hooks below throw outside the provider. init sends sign-in back to the app root, so it needs no callback page: the provider finishes sign-in on whichever page the redirect lands on.

Now skip to Add sign-in. Needs @buildbase/sdk 0.0.79 or later; what it will and will not touch, and the Pages Router case, are in Installation.

By hand: the files to create

The with-buildbase example is these files in a running app, with sign-in returning to the app root instead of /callback.

1. Install and set the environment

npm install @buildbase/sdk
# .env.local
NEXT_PUBLIC_BUILDBASE_SERVER_URL=https://api.console.buildbase.app
NEXT_PUBLIC_BUILDBASE_ORG_ID=your-24-character-org-id
NEXT_PUBLIC_BUILDBASE_CLIENT_ID=your-client-id
NEXT_PUBLIC_BUILDBASE_REDIRECT_URL=http://localhost:3000/callback
BUILDBASE_CLIENT_SECRET=your-client-secret

Add http://localhost:3000/callback to your client's redirect URLs in the console. The secret has no NEXT_PUBLIC_ prefix, so only your route handlers can read it. The organization ID must be the real 24-character value: the provider throws on anything else.

2. Create the provider

// app/providers.tsx
'use client';

import '@buildbase/sdk/css';

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

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SaaSOSProvider
      serverUrl={process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL!}
      version={ApiVersion.V1}
      orgId={process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!}
      auth={{
        clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID!,
        redirectUrl: process.env.NEXT_PUBLIC_BUILDBASE_REDIRECT_URL!,
        callbacks: {
          getSession: async () => {
            const res = await fetch('/api/auth/session');
            const data = (await res.json()) as { sessionId: string | null };
            return data.sessionId ?? null;
          },
          handleAuthentication: async (code: string) => {
            const res = await fetch('/api/auth/verify', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ code }),
            });
            const data = (await res.json()) as { sessionId: string };
            return { sessionId: data.sessionId };
          },
          onSignOut: async () => {
            await fetch('/api/auth/signout', { method: 'POST' });
          },
        },
      }}
    >
      {children}
    </SaaSOSProvider>
  );
}

'use client' is required. Every file under app/ is a Server Component by default, and a Server Component cannot hand functions such as these callbacks to the provider as props.

3. Wrap the root layout

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Keep your own fonts, metadata and global styles; the change is the <Providers> wrapper.

4. Add the three auth routes

The provider calls these. The code exchange needs your client secret, which is why it runs on your server.

// app/api/auth/verify/route.ts
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { code } = (await request.json().catch(() => ({}))) as {
    code?: string;
  };
  if (!code)
    return NextResponse.json({ error: 'Missing code' }, { status: 400 });

  const res = await fetch(
    `${process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL}/api/v1/auth/token`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        code,
        orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID,
        clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID,
        clientSecret: process.env.BUILDBASE_CLIENT_SECRET,
      }),
    }
  );
  if (!res.ok)
    return NextResponse.json({ error: 'Sign-in failed' }, { status: 401 });

  const { data } = (await res.json()) as { data: { sessionId: string } };
  const response = NextResponse.json({ sessionId: data.sessionId });
  response.cookies.set('bb-session', data.sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 30,
  });
  return response;
}
// app/api/auth/session/route.ts
import { NextRequest, NextResponse } from 'next/server';

export function GET(request: NextRequest) {
  return NextResponse.json({
    sessionId: request.cookies.get('bb-session')?.value ?? null,
  });
}
// app/api/auth/signout/route.ts
import { NextResponse } from 'next/server';

export function POST() {
  const response = NextResponse.json({ ok: true });
  response.cookies.delete('bb-session');
  return response;
}

5. Create the callback page

The hosted sign-in page sends people back to your redirect URL with a one-time code. The provider sees it, calls /api/auth/verify, and then returns them to the page where they pressed Sign in. The page only has to exist:

// app/callback/page.tsx
import Link from 'next/link';
import { WhenAuthenticated, WhenUnauthenticated } from '@buildbase/sdk/react';

export default function CallbackPage() {
  return (
    <main>
      <WhenAuthenticated loadingComponent={<p>Signing you in...</p>}>
        <Link href="/">Continue</Link>
      </WhenAuthenticated>
      <WhenUnauthenticated>
        <p>
          Sign-in did not finish. <Link href="/">Back to the home page</Link>
        </p>
      </WhenUnauthenticated>
    </main>
  );
}

This page needs no 'use client': it calls no hook and passes no function, and the SDK's components are client components already.

Add sign-in

Any component that calls a hook needs 'use client'.

// app/account.tsx
'use client';

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

export function Account() {
  const { user, signIn, signOut } = useSaaSAuth();

  return (
    <>
      <WhenUnauthenticated>
        <button onClick={() => signIn()}>Sign in</button>
      </WhenUnauthenticated>

      <WhenAuthenticated>
        <p>Welcome, {user?.name}</p>
        <button onClick={() => signOut()}>Sign out</button>
      </WhenAuthenticated>
    </>
  );
}

Render <Account /> from any page, for example app/page.tsx. Write onClick={() => signIn()}, not onClick={signIn}: signIn takes an optional return URL, and passing it straight to onClick hands it the click event instead, which TypeScript rejects. signIn('/dashboard') sends people to /dashboard after they sign in.

Add a workspace switcher

// app/team-header.tsx
'use client';

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

export function TeamHeader() {
  return (
    <WorkspaceSwitcher
      trigger={(isLoading, workspace) => (
        <span>
          {isLoading ? 'Loading...' : (workspace?.name ?? 'Select workspace')}
        </span>
      )}
    />
  );
}

The switcher renders its own button around trigger, so return text or a <span>, not another <button>. Its workspaces load once someone is signed in; put it inside WhenAuthenticated to hide it before then.

Open billing settings

// app/billing-button.tsx
'use client';

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

export function BillingButton() {
  const { openWorkspaceSettings } = useSaaSAuth();

  return (
    <button onClick={() => openWorkspaceSettings('subscription')}>
      Manage subscription
    </button>
  );
}

openWorkspaceSettings needs a current workspace, so render this button for signed-in users.

What happened

  1. SaaSOSProvider connects your app to BuildBase and sets up auth, workspace context and permissions.
  2. signIn() sends people to the hosted sign-in page. They come back to your redirect URL with a code, your /api/auth/verify route swaps it for a session in an httpOnly cookie, and /api/auth/session restores it after a refresh.
  3. WhenAuthenticated and WhenUnauthenticated render UI by auth state, with no manual checks.
  4. WorkspaceSwitcher is a drop-in component for switching between workspaces.
  5. openWorkspaceSettings('subscription') opens the pre-built billing screen.

Next Steps

  • Going to Production - Env vars, project structure, and the deployment checklist.
  • Authentication - Sign-in, social login, session management.
  • Workspaces - Create workspaces, manage members, handle invites.
  • Billing - Subscription plans, checkout, usage tracking.
  • Permissions - Role-based access control and conditional rendering.