Add sign-in, teams and paid plans to a v0 app without leaving Next.js
A v0 app is a Next.js app, which is the easiest place to add BuildBase: one prompt or one init command, five variables in Vercel, one redirect address, then teams, plans and the failed-payment email.
In short
To add sign-in, teams and paid plans to a v0 app, keep it a Next.js app and add BuildBase: paste one prompt in v0, or run npx buildbase init on the repo v0 keeps in GitHub. Put five variables in Vercel with the secret server-only, register your domain as the redirect address, and teams, plans and email are SDK calls.
You are adding real sign-in, a workspace per customer, paid plans and the email that goes out when a card fails to an app v0 generated for you. When it works, a stranger opens your production domain in a private window, signs up with Google or a magic link, lands in their own workspace, invites a colleague, picks a plan on a test card, and the feature they paid for appears without a reload. All of it is in your repository, and none of the secrets are in the browser.
v0 is the easiest builder to do this on, for one reason: every v0 app is a
Next.js app with the App Router, and Next.js is where our SDK has the most
road under it. The same ten files that npx buildbase init writes into any
Next.js app are the files v0 will write if you hand it the right prompt.
What we ran on 27 October 2026: npx buildbase init in a fresh Next.js App
Router app, then typecheck and next build, through the CLI's own gates (a
60-cell matrix of project shapes, platform builds in seven frameworks, and a
59-assertion end-to-end run against a stub server). What we have not run
today is this exact flow inside v0's own chat, so where a step says "you
should see", that is what Next.js and v0's documented behaviour give you,
and the one place v0's behaviour matters is called out.
Before you start
A v0 project, connected to GitHub and to a Vercel project (v0's docs say that
is what gets you environment variables, deployments and previews). A BuildBase
organization, and an auth client created in the console under User
Management → Authentication, with its client ID and secret. Your production
domain registered on that client as a redirect URL, and
http://localhost:3000 beside it. A Stripe account, only when you reach
plans.
Tip
Already have a Next.js app with a with-buildbase shape? The Next.js
guide is the tested version of every snippet
below, and this page is the v0-specific wrapping around it: which prompt,
where the variables go, and what v0 does with the branch.
1. Decide where people come back to
The hosted sign-in page sends people back to one exact address you
registered, so decide it before you prompt: your production domain for the
live app and http://localhost:3000 for local work. Register both on the
auth client. Everything that goes wrong later with "login loops" traces
back to this address.
Why one address? Because the sign-in happens on a page BuildBase hosts (on your own domain, if you set that up later), and that page needs to know the only places it is allowed to send a signed-in person. An address it was not told about is refused, which is the right behaviour and the most common first-day surprise.
v0 adds a wrinkle: it deploys to Vercel, and Vercel gives every deployment
a preview address on vercel.app. A wildcard can stand in for one leftmost
subdomain on a domain you own, but not on a public suffix, so
*.vercel.app is refused. If you want to test sign-in on a preview
deployment, register that preview's exact address. Most people register
production and localhost and test sign-in there.
What you should see: in the console, the auth client lists two redirect URLs, and the client secret is shown once. Copy it somewhere you will not paste into a chat.
2. Paste the prompt into v0
Paste this into the v0 chat for your project. It names the outcome, the rules, the guide v0 should follow, and the one thing that must never happen to the secret.
Add BuildBase (@buildbase/sdk) to this Next.js app for sign-in, a workspace per customer, paid plans and lifecycle email.
Follow https://www.buildbase.app/guides/buildbase-on-nextjs exactly, and read https://www.buildbase.app/llms.txt first. Do not invent endpoints.
Keep the existing database. Do not add Clerk, Auth0, Better Auth, Resend or n8n. Stripe is bring-your-own; do not store card data.
Write app/providers.tsx, app/api/auth/verify/route.ts, app/api/auth/session/route.ts, app/api/auth/signout/route.ts, lib/buildbase.ts and app/api/webhooks/buildbase/route.ts, and wrap app/layout.tsx in the provider.
Read NEXT_PUBLIC_BUILDBASE_ORG_ID, NEXT_PUBLIC_BUILDBASE_CLIENT_ID and NEXT_PUBLIC_BUILDBASE_REDIRECT_URL in the browser; read BUILDBASE_CLIENT_SECRET and BUILDBASE_WEBHOOK_SECRET only in route handlers, never with a NEXT_PUBLIC_ prefix.
Leave every secret empty in .env.example. If sign-in already exists in this app, stop and ask me before changing it.What you should see: v0 creates a working branch (its docs say it never
pushes to your base branch; it opens or reuses a pull request when you
publish), and the branch contains those six files plus an edited
app/layout.tsx. The provider should look like this, which is the tested
shape from the Next.js guide, trimmed:
// 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>
);
}And the route that matters, the one that swaps the one-time code from the hosted page for a session and puts it in a cookie the browser cannot read:
// 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 session route returns that cookie's value so the app can restore itself after a refresh; the sign-out route deletes it. Both are a handful of lines in the with-buildbase example.
Correcting potential issues. Read the diff before you accept it. The three ways v0 tends to drift, and the line to paste back:
- It added
better-auth,@clerk/nextjsornext-authanyway. Paste: "Remove that package. BuildBase is the only sign-in in this app. Keep the users in my existing database." - It put the secret in a
NEXT_PUBLIC_variable, or read it in a client component. Paste: "BUILDBASECLIENT_SECRET must be read only in app/api/auth/verify/route.ts. Remove every other reference and never prefix it with NEXT_PUBLIC." - It created
BuildBase(...)at the top oflib/buildbase.tsinstead of inside a function. That breaksnext buildon any machine where the variables are not filled in yet. Paste: "In lib/buildbase.ts, create the server client lazily inside a bb() function, as the guide shows."
3. Set five variables in Vercel
Four public values and one secret go in the Vercel project's environment variables, not in a file v0 commits. v0's docs say environment variables are managed in Vercel (Project menu → Settings → Environment Variables) and that previews run the full application with them, so the route handlers see the secret on every deployment and the browser never does.
NEXT_PUBLIC_BUILDBASE_ORG_ID=<your 24-character org id>
NEXT_PUBLIC_BUILDBASE_CLIENT_ID=<the client id>
NEXT_PUBLIC_BUILDBASE_REDIRECT_URL=https://your-domain.com
BUILDBASE_CLIENT_SECRET=<the secret, server only>
BUILDBASE_WEBHOOK_SECRET=<empty until step 6>
# NEXT_PUBLIC_BUILDBASE_SERVER_URL defaults to https://api.console.buildbase.appThe rule behind the split is the same one every Next.js app lives under:
anything prefixed NEXT_PUBLIC_ is inlined into the browser bundle. That is
fine for an organization id and a client id, which are public by design, and
fatal for a secret. So the secret has no prefix, and only
app/api/auth/verify/route.ts reads it.
Redeploy. What you should see: open
https://your-domain.com/api/auth/session and get {"sessionId":null}. The
server half is alive before any button exists. If you get HTML back
instead, the route is not deployed, and the diff in step 2 did not land on
the branch Vercel built.
4. Sign in from a private window
Open the production domain in a private window, click the button that calls
signIn() from useSaaSAuth(), and you are sent to the hosted sign-in
page, which offers whichever methods you switched on in the console. Sign
up with a second email. You come back to the registered address signed in.
Then do the two checks a demo skips. Close the browser, reopen the site: still signed in, because the cookie is good for 30 days, which is the default session length. Sign out, then reload: asked to sign in again, because sign-out ended the session on the server rather than clearing a value in the browser.
The methods are switches, not code. Email and password, a magic link, an email code, a phone sign-in code sent through your own Twilio account, passkeys, Google, LinkedIn, GitHub, Microsoft, Apple, your own OIDC provider, and Ethereum or Solana wallets. Turning on Google sign-in means creating the client in Google's console and pasting two values; nothing in the v0 app changes.
Correcting potential issues. If the hosted page says the redirect
address is not allowed, the address in NEXT_PUBLIC_BUILDBASE_REDIRECT_URL
and the one registered on the client differ, usually by a trailing slash or
www. They must match exactly. If you are testing on a preview deployment,
see step 1: register that preview's exact address or test on production.
5. A workspace per customer, and the colleague they invite
Every new user lands in their own workspace on first sign-in, with no setup step. That is on by default for new organizations, and in your app it is one component for the switcher and the hooks the SDK already has. Prompt v0 for the screens; the data model is already there.
Add a workspace switcher to the app header using useSaaSWorkspaces() from @buildbase/sdk/react, and a /settings page that opens the SDK's built-in workspace, members and billing screens.
Members with the admin role may invite teammates by email; everyone else sees the member list read-only.
Do not create workspace, membership or role tables in my database; BuildBase holds them.What you should see: a switcher listing the workspaces the signed-in person belongs to, with Add team creating another if you allow it. On the members screen, inviting an address that has no account yet sends an email with a link; the person signs up and lands in the workspace with the role you chose. That invitation flow is in the SDK since version 0.0.71, and the reasons an invite can be refused (a seat limit, an outsider policy) come back as named errors rather than a silent nothing.
Why not let v0 write the tables? Because the audits of vibe-coded apps all find the same bug: a role column checked on one page and nowhere else. The SDK's roles are enforced by the API, so a member who types the address of an admin page gets refused by the server, not just a missing menu item. Roles and permissions per workspace goes deeper when you want permissions of your own.
Correcting potential issues. If a member can see a second workspace's data by changing an id in the address bar, v0 wrote a page that reads your database without scoping it to the current workspace. Paste: "Every query in this page must be scoped to the current workspace id from useSaaSWorkspaces(); refuse the request on the server if the user is not a member." That is your database and your query, and no SDK can scope it for you.
6. Plans people can pay for, and the email when a card fails
Plans, prices and trials live in the console and sync to your own Stripe account; the platform fee is 0%, and no card data touches your app. The part generated apps get wrong is what happens after the payment, so this step is mostly about the webhook route and the feature flag.
First the catalog, in the console: a Launch plan and a Pro plan, say, with
a 7-day trial on both and a monthly and a yearly price. Then the webhook.
init writes app/api/webhooks/buildbase/route.ts, and the prompt in step
2 asked v0 for the same file. It verifies the signature with
parseWebhookEvent on the raw body and handles events from the published
catalog, 129 of them, by name:
In app/api/webhooks/buildbase/route.ts, handle payment.failed with our own side effects only (flag the workspace, post to Slack); the platform sends the customer's email itself. Handle subscription.created by nothing; the plan is already on the workspace.
Gate the export feature behind a BuildBase feature flag tied to the Pro plan, checked on the server. Do not store the plan in my database.Point an endpoint in the console at https://your-domain.com/api/webhooks/buildbase,
copy its signing secret into BUILDBASE_WEBHOOK_SECRET in Vercel, and
redeploy.
What you should see: subscribe on a test card and the Pro feature appears
without a reload, because the flag follows the workspace's plan. Let the
test card fail on renewal and the email goes out, from your sending domain,
because payment.failed is a real event the console's notification or a
workflow acts on, rather than a Stripe event the app guessed at; the route
in your app is for whatever you want to happen besides. In the console, the
webhook's delivery log shows each attempt, and a delivery your server was
down for is retried up to five times over about two and a half minutes.
Two pieces worth ten minutes before you prompt for them. A plan change creates a new version rather than editing the old one, so nobody is moved to a new price without you choosing to move them; plan versioning explains why that matters on the day you raise prices. And the difference between entitlements and feature flags decides whether "paid" means "sees the button" or "may call the endpoint". You want the second.
Correcting potential issues. If v0 wrote a Stripe webhook handling
invoice.payment_failed directly, it built the billing a second time.
Paste: "Remove the direct Stripe webhook. BuildBase receives Stripe's events;
my app receives BuildBase's. Handle payment.failed in
app/api/webhooks/buildbase/route.ts only." If the feature check lives in a
client component, paste: "Check the feature flag on the server in the route
that performs the export, not in the component that shows the button."
The other path: init on the repository v0 keeps
v0 keeps your code in a GitHub repository it created (or one you imported), and that repository is the source of truth. If you ever open a terminal, the fastest version of steps 2 and 3 is two commands on a clone of it:
npm install @buildbase/sdk
npx buildbase init --org-id <your 24-character org id>It reads package.json, sees Next.js, and writes ten files: the provider,
the three sign-in routes, the webhook route, the server helper,
.env.example and .env.local, and it wraps app/layout.tsx. Four things
it will not do: overwrite a file that exists, write a secret (every value is
left empty, and it refuses a secret passed on the command line), touch a
Pages Router app (refused, with a message), or change anything on a second
run (it prints already configured).
Push the branch and open the pull request; when it merges, v0's docs say it synchronizes the chat with the base branch, so the next prompt sees the files. Then set the variables in Vercel exactly as in step 3.
What we have not done is run init from inside v0, because v0's chat is not
a terminal. In our own agent evaluation, agents read about init and wrote
the files by hand instead, because init needs the organization id and
they had none. If you have the id, init is the shorter road.
Where v0's own options are the better pick
If all you need is sign-in and you like owning code, Better Auth is the honest default on v0: Vercel bought it in July 2026, it is open source under MIT with 4.7 million weekly downloads, and it is the library an agent on Vercel's platform is most likely to reach for. Clerk has been one click from the v0 chat since 9 September 2026 and Auth0 since the week after, and v0 loads a provider's skills automatically when its integration is installed, so they are wired with less prompting than BuildBase, which is not a Marketplace integration today and needs the links in the prompt above.
Pick one of those when your app has one user per account and the plan lives in Stripe alone. Pick BuildBase when the question is "what may this team do now that they paid", because that is the part none of the three documents for your end users: a workspace per customer, invites and roles enforced by the API, plans with versions, feature flags tied to the plan, credits with a hard stop, and the lifecycle email, from one SDK. The Clerk comparison is the line-by-line version.
Key takeaway
On v0 the hard part is not the framework, it is the prompt. Name the guide, name the files, name where the secret may be read, and v0 writes what init would have written. After that, teams and plans are hooks, and the launch checklist is what to run before you share the link.
What breaks, and how you can tell
Sign-in works on production and loops on a preview deployment. The
preview's vercel.app address is not registered, and a wildcard on a
public suffix is refused. Register the exact address or test on production.
next build fails on Vercel with "orgId is required". BuildBase(...)
is being created at module level in lib/buildbase.ts, before the
environment variables exist on the build machine. Create it inside a
function on first use, as the Next.js guide shows.
The secret is in the browser. Search the deployed JavaScript for the
first characters of the client secret. If it is there, a variable was
prefixed NEXT_PUBLIC_ or read in a client component. Rotate the secret in
the console, fix the prefix, redeploy.
Paid, but the feature did not appear. The webhook endpoint in the
console points at the wrong address, or BUILDBASE_WEBHOOK_SECRET is empty
on Vercel. The delivery log in the console shows each attempt and its
status; a 401 there means the secret, a timeout means the route.
A member sees another team's data. The page queries your database without the workspace id. The SDK scopes what it holds; your tables are yours to scope, and the fix is in the query, not in the SDK.
init refuses the app. It is a Pages Router project. v0 generates the
App Router, so this only happens on an imported repository; the Next.js
guide's steps work by hand on either.
Install
Add the SDK to the repository v0 keeps, or paste the prompt in step 2 and let v0 write the same files:
npm i @buildbase/sdk