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.
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.appOnly 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:
- Sign-in methods. Magic link and passkeys are toggles, not code. If protected pages bounce you back to login forever, see the middleware redirect loop.
- Plans. A free trial without a card, an annual and monthly toggle, proration on plan change, changing prices without touching existing customers and charging per seat.
- Usage. A credits wallet, charging per credit, charging per API call and enforcing quotas on the server.
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
josesessions, 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