Guides
Build guide

BuildBase on Next.js: add auth, teams and billing to the app you already have

Add auth, teams and billing to an existing Next.js app: the setup, getting the session to server components, and three popular templates swapped over.

Dharmendra Jagodana5 min read

In short

To add auth and billing to an existing Next.js app with BuildBase, send users to hosted sign-in, swap the returned code for a session on the server, and keep it in an httpOnly cookie that server code reads through getSessionId. After that, teams, plans and credits are ordinary SDK calls in client and server code alike.

Every auth, billing and teams guide for Next.js starts from a blank app. Yours is not blank. It has a users table, a login page someone wrote in a hurry, maybe a Stripe webhook that mostly works.

Here is the part those guides skip: in Next.js, the only thing that differs from any other stack is how the session reaches a server component. Get those thirty-odd lines right once and everything after them - workspaces, plans, credits, roles - is the same SDK call you would make anywhere else.

We checked that the unglamorous way. We pulled BuildBase into three templates people already clone, then deleted whatever it replaced and counted the lines.

Before you start

A Next.js app on the App Router, a BuildBase organization, and an auth client created in the console under User Management → Authentication, with its client ID and secret. Register your callback URL on that client, for example http://localhost:3000, exactly as your app serves it.

Tip

Start from the working example. with-buildbase in the examples repo is the five steps below and nothing else. Watch it working, or copy it with npx degit buildbase-app/examples/with-buildbase my-app. For a full app, start from saas-starter (teams and plans), ai-chatbot (credits per message) or nextjs-boilerplate (the Clerk swap).

1. Install the SDK and set five variables

npm i @buildbase/sdk
# .env.local
NEXT_PUBLIC_BUILDBASE_ORG_ID=demo-org-id
NEXT_PUBLIC_BUILDBASE_CLIENT_ID=demo-client-id
NEXT_PUBLIC_BUILDBASE_REDIRECT_URL=http://localhost:3000
BUILDBASE_CLIENT_SECRET=demo-client-secret   # server only, never NEXT_PUBLIC_
# NEXT_PUBLIC_BUILDBASE_SERVER_URL defaults to https://api.console.buildbase.app

Only the secret goes without the NEXT_PUBLIC_ prefix. Next.js inlines anything with that prefix into the client bundle, which is the one place a secret cannot go.

2. Wrap the app in the provider

The React SDK needs three callbacks from you: where to get the current session, what to do with the one-time code the hosted page returns, and how to sign out. All three talk to your own route handlers, which you write in the next step.

// 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 ||
        'https://api.console.buildbase.app'
      }
      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 () =>
            (await (await fetch('/api/auth/session')).json()).sessionId,
          handleAuthentication: async (code: string) => {
            const res = await fetch('/api/auth/verify', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ code }),
            });
            return { sessionId: (await res.json()).sessionId };
          },
          onSignOut: async () => {
            await fetch('/api/auth/signout', { method: 'POST' });
          },
        },
      }}
    >
      {children}
    </SaaSOSProvider>
  );
}

Render <Providers> around {children} in app/layout.tsx. A button that calls signIn() from useSaaSAuth() now sends people to the hosted sign-in page, which offers whichever of the eight sign-in methods your org has switched on.

3. Swap the code for a session, on the server

The hosted page comes back with a one-time code. Exchanging it needs the client secret, so it happens in a route handler. Whatever session comes back goes straight into an httpOnly cookie.

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

export async function POST(request: Request) {
  const { code } = await request.json();
  const res = await fetch(
    `${process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || 'https://api.console.buildbase.app'}/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();
  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;
}

The 30-day maxAge matches BuildBase's default session length. Two shorter routes finish the set: GET /api/auth/session returns the cookie's value so the client can restore itself after a refresh, and POST /api/auth/signout deletes it. Both are a handful of lines in the with-buildbase example. A single-page app with no server of its own needs the same three, and BuildBase on Vite runs them as serverless functions.

4. Teach the server SDK where the session lives

This is the Next.js-specific part. The server SDK takes a getSessionId function, and in Next.js that function reads the cookie:

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

export async function getSessionId() {
  return (await cookies()).get('bb-session')?.value ?? null;
}

let client: ReturnType<typeof BuildBase> | undefined;

export function bb() {
  client ??= BuildBase({
    serverUrl:
      process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL ||
      'https://api.console.buildbase.app',
    orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!,
    getSessionId,
  });
  return client;
}

Why a function rather than BuildBase(...) at the top of the file? Because BuildBase() refuses to start without an org ID, and a module-level call breaks next build on any machine where the env is not filled in yet. Creating it on first use keeps the build green.

5. Read the user in a server component

// app/profile/page.tsx
import { redirect } from 'next/navigation';

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

export const dynamic = 'force-dynamic';

export default async function ProfilePage() {
  const profile = await bb()
    .users.getProfile()
    .catch(() => null);
  if (!profile) redirect('/');

  return <p>Signed in as {profile.email}</p>;
}

Sign in, open /profile, and the email comes from BuildBase via your server, with nothing in client state. Delete the cookie in dev tools and reload: you are sent home. That round trip is the whole integration. The same bb() works in a route handler or a server action without changes.

Key takeaway

In Next.js, BuildBase is a cookie and a getSessionId function. Everything after that is the same call you would make from any other framework.

6. Then add the part you actually came for

With the session flowing, each feature is its own guide. They all assume the five steps above:

What it replaced in three real templates

Guides on blank apps prove the API works. They do not show what you get to delete. So we took three templates with 50,000 GitHub stars between them and swapped BuildBase in, one folder each in the examples repo:

  • saas-starter, from nextjs/saas-starter (16k stars). Its bcrypt and jose sessions, the teams, members, invitations and activity-log tables, and the Stripe checkout, portal and webhook all went. So did Postgres: the app's TypeScript dropped from 3,586 lines to 1,773.
  • nextjs-boilerplate, from ixartz/Next-js-Boilerplate (13k stars). The Clerk swap: the route guard, provider, sign-in and sign-up pages, user profile and currentUser() calls. Two dependencies out, one in.
  • ai-chatbot, from vercel/chatbot (21k stars). NextAuth and guest accounts are replaced, and the "10 messages per user per hour" cap becomes credits: every message calls bb.credits.consume() with the message ID as its idempotency key, and an empty workspace gets a 402.

Each README says what changed, what stayed upstream's, and what the swap does not cover. The saas-starter one admits a real gap: inviting someone who has no account yet is not in the published SDK.

What breaks, and how you can tell

The hosted page refuses to send you back. The URL in NEXT_PUBLIC_BUILDBASE_REDIRECT_URL must match one registered on the auth client character for character, scheme and path included; the port must match too, except on localhost, where it is ignored. A wildcard can stand in for one leftmost subdomain on a domain you own (https://*.app.example.com/...), but not for a public suffix, so *.vercel.app is refused: register each preview domain you use.

The profile page shows the last user who loaded it. Next.js cached the page. Anything that reads the session cookie must render per request: mark it force-dynamic, or read cookies() before you fetch.

cookies() wants an await on one version and not the other. It is synchronous in Next.js 14 and asynchronous from 15. The three templates above run 15 and 16, so they await it; on 14, drop the await. Next.js 16 also renames middleware.ts to proxy.ts, which is why the boilerplate's route guard lives there.

Credits stop spending under load, with 429s. Credit consumption is limited to 30 requests a minute per IP. Serverless functions can share an outgoing IP, so a busy Vercel deployment can hit that where a single server would not.

The secret shows up in your client bundle. Someone added NEXT_PUBLIC_ to BUILDBASE_CLIENT_SECRET. Treat the secret as leaked: create a new auth client secret, then remove the prefix.

Install

Add the SDK to your own app, or start from the example with npx degit buildbase-app/examples/with-buildbase my-app.

npm i @buildbase/sdk
nextjs
auth
billing

Frequently Asked Questions

Do I have to start from a new starter kit?

No. The examples repo swaps BuildBase into three templates people already use - nextjs/saas-starter, ixartz/Next-js-Boilerplate and vercel/chatbot - replacing the auth, teams and billing code each one already had, and leaving the rest of the app alone.

How does a server component know who the user is?

The BuildBase session ID sits in an httpOnly cookie. You give the server SDK a getSessionId function that reads that cookie, so every call from a server component, route handler or server action runs as the signed-in user, and the session never reaches client JavaScript.

Can I charge per message in the Vercel AI chatbot?

Yes. The ai-chatbot example spends credits on every message with bb.credits.consume(), using the message ID as the idempotency key, and answers 402 when the workspace runs out so the UI can open the credit store.

Ship it

Create a project and run this guide against your own workspace.

7-day free trialNo credit card requiredCancel anytime