BuildBaseBuildBase

Going to Production

Everything between the 5-minute quickstart and a production-ready SaaS app.

The quickstart gets you a working demo. Here is what to configure before real users sign in.

1. Environment variables

Create a .env.local file in your project root. Never commit this file.

# BuildBase
NEXT_PUBLIC_BUILDBASE_SERVER_URL=https://api.console.buildbase.app
NEXT_PUBLIC_BUILDBASE_ORG_ID=your-org-id
NEXT_PUBLIC_BUILDBASE_CLIENT_ID=your-client-id
NEXT_PUBLIC_BUILDBASE_REDIRECT_URL=http://localhost:3000/callback

# Server-only — never expose with a NEXT_PUBLIC_ prefix
BUILDBASE_CLIENT_SECRET=your-client-secret

# Session secret (used to sign httpOnly cookies)
SESSION_SECRET=run-openssl-rand-hex-32

Update your provider to read from env:

<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: { /* ... */ },
  }}
>

2. Auth callback API route

The quickstart shows fetch('/api/auth/verify') — here's the actual implementation. This route calls BuildBase's token endpoint (POST /api/v1/auth/token) to exchange the auth code for a session, then sets an httpOnly cookie. The exchange requires your clientSecret, which is why it runs on your backend — the secret must never reach the browser.

// app/api/auth/verify/route.ts
import { cookies } from 'next/headers';

export async function POST(req: Request) {
  const { code } = await req.json();

  // Exchange the auth code for a session
  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 Response.json({ error: 'Invalid or expired code' }, { status: 401 });
  }

  const { data } = await res.json(); // { sessionId, user }

  // Set httpOnly cookie (secure in production)
  const cookieStore = await cookies();
  cookieStore.set('bb-session', data.sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 30, // 30 days
  });

  return Response.json({ sessionId: data.sessionId });
}
// app/api/auth/session/route.ts
import { cookies } from 'next/headers';

export async function GET() {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get('bb-session')?.value ?? null;
  return Response.json({ sessionId });
}
// app/api/auth/signout/route.ts
import { cookies } from 'next/headers';

export async function POST() {
  const cookieStore = await cookies();
  cookieStore.delete('bb-session');
  return Response.json({ ok: true });
}

3. Project structure

A typical Next.js + BuildBase project:

app/
├── api/
│   └── auth/
│       ├── verify/route.ts     ← exchanges code for session
│       ├── session/route.ts    ← returns session from cookie
│       └── signout/route.ts    ← clears session cookie
├── (auth)/
│   ├── login/page.tsx          ← public pages
│   └── register/page.tsx
├── (app)/
│   ├── layout.tsx              ← wraps in BuildBaseProvider
│   ├── dashboard/page.tsx      ← protected pages
│   ├── settings/page.tsx
│   └── billing/page.tsx
├── layout.tsx                  ← root layout (fonts, global styles)
└── providers.tsx               ← BuildBaseProvider component

4. Protect your routes

Wrap your authenticated layout so unauthenticated users can't access app pages:

// app/(app)/layout.tsx
'use client';

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

export default function AppLayout({ children }: { children: React.ReactNode }) {
  const { isLoading, signIn } = useSaaSAuth();

  if (isLoading) return <LoadingSpinner />;

  return (
    <>
      <WhenAuthenticated>{children}</WhenAuthenticated>
      <WhenUnauthenticated>
        <button onClick={() => signIn()}>Sign In</button>
      </WhenUnauthenticated>
    </>
  );
}

5. Dashboard setup checklist

Before going live, make sure these are configured in the BuildBase dashboard:

StepWhereWhat to do
Create orgDashboard homeClick "Create Organization"
Get credentialsSettings → GeneralCopy orgId
Set up OAuthUser Management → Access → AuthenticationCreate a client, get clientId and clientSecret, add redirect URLs
Add auth methodsUser Management → Access → Authentication → MethodsEnable Email/Password, Google, or Magic Link
Create plansBilling → PlansSet up at least one plan (if using billing)
Connect StripeBilling → CredentialsEnter Stripe API keys (test keys for dev)
Set up email senderMessaging → Sender AccountsConfigure Google, Mailgun, or SMTP
Add redirect URLsUser Management → Access → AuthenticationAdd your production URL (e.g. https://yourapp.com/callback)

6. Production environment

For production, update your environment variables:

# .env.production
NEXT_PUBLIC_BUILDBASE_SERVER_URL=https://api.console.buildbase.app
NEXT_PUBLIC_BUILDBASE_ORG_ID=your-org-id
NEXT_PUBLIC_BUILDBASE_CLIENT_ID=your-client-id
NEXT_PUBLIC_BUILDBASE_REDIRECT_URL=https://yourapp.com/callback

BUILDBASE_CLIENT_SECRET=your-client-secret
SESSION_SECRET=your-production-secret

Key differences from development:

  • REDIRECT_URL must be your production domain (not localhost)
  • Add the production redirect URL in the dashboard under User Management → Access → Authentication
  • Use real Stripe keys instead of test keys
  • Set secure: true on session cookies (already handled by the NODE_ENV check above)

Multi-tenant apps on subdomains

If every tenant gets its own subdomain (acme.app.example.com), you do not need one redirect URL per tenant. Register a single wildcard instead:

https://*.app.example.com/callback

The * stands for exactly one label, so acme.app.example.com and globex.app.example.com both match while a.b.app.example.com does not. Everything else is still compared exactly - scheme, port and path - and the wildcard is only allowed as the first label of the domain, never in the path.

A few rules worth knowing before you rely on it:

  • https only, except on localhost, where http://*.localhost:3000/callback works so you can test tenant subdomains locally.
  • The domain must be one you could own. *.com, *.co.uk and *.vercel.app are rejected - they are public suffixes, not domains.
  • Wildcards are console-only. Clients that register themselves through Dynamic Client Registration cannot request a wildcard; an admin has to add it.

The same pattern also covers social login and magic links from tenant subdomains, so those work without extra entries too.

7. Deployment checklist

Before your first deploy:

  • Environment variables set in your hosting platform (Vercel, Railway, etc.)
  • Production redirect URL added in BuildBase dashboard
  • Auth methods enabled (email, social, magic link)
  • At least one plan created and published (if using billing)
  • Stripe connected with live keys (if using billing)
  • Email sender configured (for transactional emails)
  • Test the full sign-in → dashboard → sign-out flow in production

Next Steps

  • Authentication — Deep dive into auth methods and sessions.
  • Billing — Set up plans, checkout, and subscriptions.
  • Server SDK — Backend operations: usage recording, credits, notifications.
  • Self-Hosted — Deploy on your own infrastructure.