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-32Update 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 component4. 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:
| Step | Where | What to do |
|---|---|---|
| Create org | Dashboard home | Click "Create Organization" |
| Get credentials | Settings → General | Copy orgId |
| Set up OAuth | User Management → Access → Authentication | Create a client, get clientId and clientSecret, add redirect URLs |
| Add auth methods | User Management → Access → Authentication → Methods | Enable Email/Password, Google, or Magic Link |
| Create plans | Billing → Plans | Set up at least one plan (if using billing) |
| Connect Stripe | Billing → Credentials | Enter Stripe API keys (test keys for dev) |
| Set up email sender | Messaging → Sender Accounts | Configure Google, Mailgun, or SMTP |
| Add redirect URLs | User Management → Access → Authentication | Add 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-secretKey differences from development:
REDIRECT_URLmust 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: trueon session cookies (already handled by theNODE_ENVcheck 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, wherehttp://*.localhost:3000/callbackworks so you can test tenant subdomains locally. - The domain must be one you could own.
*.com,*.co.ukand*.vercel.appare 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.