Development and Staging Environments
Give your app development and staging environments, each with its own database, users and keys, and promote configuration to production.
What it does
An environment is a separate tenant of your app that shares nothing with production but your team. Each one has its own database, users, sessions, API keys, webhooks, senders and files. Production is your organization itself, as it always was: an app that sets no environment talks to production.
Before you start
Sign the CLI in to your console once: npx buildbase login. Adding an
environment needs an owner or admin.
Warning
Environments are rolling out to organizations. Yours has them once the console shows Environments under Settings > Organization. Until then, buildbase env create is refused, and your app keeps running in production as it always has.
Requires @buildbase/sdk 1.0.0 or later. buildbase login, init --dev, the env commands and the environment option on this page all arrive with it. An older SDK ignores environment without an error and sends every request to production: sign-in with the environment's client is refused as an invalid client id, and anything that needs no client, such as your plans, is read from production.
From your terminal
npx buildbase init --dev # wire to Development
npx buildbase env create Staging # add Staging, point to it
npx buildbase env list # your environments
npx buildbase env use Development # point to another oneinit --dev and env create write the environment into .env.local: the server URL, your org id, the environment's key, its sign-in client and, in .env.local only, the client secret. The secret is handed out once per environment; a teammate who runs --dev later copies it, or passes --rotate-secret for a new one, and the old one stops working.
You can also add an environment in the console: Settings > Organization > Environments > Add environment, or Add environment in the environment switcher.
Point the SDK at it
The provider and the server client take one option, the environment's key:
<SaaSOSProvider
serverUrl={process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL}
version="v1"
orgId={process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID}
environment={process.env.NEXT_PUBLIC_BUILDBASE_ENVIRONMENT}
>
<App />
</SaaSOSProvider>import BuildBase from '@buildbase/sdk';
const bb = BuildBase({
serverUrl: process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL,
orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID,
environment: process.env.NEXT_PUBLIC_BUILDBASE_ENVIRONMENT,
});The key is eight lowercase letters, shown in the console. Unset, or production, means production. It is never guessed from NODE_ENV: a preview deployment runs with NODE_ENV=production, so it reaches production only when you leave the key out on purpose. Any other value throws when the app starts, so a typo never sends requests somewhere you did not mean.
The same option is on createAgentStack (your MCP server, which lets AI agents use your app) and on a standalone BuildBaseTracking. In the browser, the keys the SDK stores things under gain :<key>: the session, device, sign-in state, current workspace and step-up (re-confirming it's you) keys. So two environments of one app on localhost keep their own sessions. With environment unset, every request, header and storage key is exactly what it was before.
From your own server routes
Some of your own routes call the BuildBase HTTP API directly, such as the token exchange behind your sign-in. Those need the environment's id. The platform knows an environment by <orgId>-<key>, and production by the bare org id. Send that id wherever the route sent orgId. environmentId() builds it, and returns the org id unchanged when the environment is unset or production:
import { environmentId } from '@buildbase/sdk';
const res = await fetch(`${serverUrl}/api/v1/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
// <orgId>-<key> in a development or staging environment
orgId: environmentId(
process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!,
process.env.NEXT_PUBLIC_BUILDBASE_ENVIRONMENT
),
clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID,
clientSecret: process.env.BUILDBASE_CLIENT_SECRET,
}),
});The token route buildbase init writes already does this. PRODUCTION_ENVIRONMENT ('production') is exported beside it. On an SDK older than 1.0.0, write the id yourself: `${orgId}-${key}`, or the org id alone for production.
Apps set up by init keep a session cookie per environment (bb-session-id-<key>), so a development build and a production build on one machine never read each other's session. An app set up by an earlier init keeps one cookie name for every environment until you give SESSION_COOKIE_NAME the key.
What each environment keeps to itself
| Separate in every environment | Shared with production |
|---|---|
| Users, sessions, workspaces, audience, lists, tags, email history, workflow runs and files | Your team: the same members, each allowed into the environments you choose |
| API keys, client secrets, webhook endpoints and their signing secrets, senders, Stripe keys | Your subscription, which only production is billed on |
| Every queued job, scheduled send, webhook delivery and cache | A suspension of your organization, which applies everywhere |
Custom sign-in and sending domains, and live Stripe keys, are set up in production only.
Ready on first use
A new environment works before you configure anything:
- Google and GitHub sign-in run through shared apps, with no OAuth app of your own.
- Mail with no sender of its own goes through a platform sender to your team's addresses only (and to people your team invites to a workspace there, never to people an app user invites), and every message the environment sends is kept in its Inbox in the console, links and codes included.
- Test mode: an address ending in
+test([email protected]) is never sent mail, and every code it asks for is 424242, so automated tests can sign up, sign in and reset a password with no mailbox. These addresses work even before the environment has a sender, since no mail is sent to them. Production never does this. Test mode is on in every environment until you turn it off: the Test sign-in codes switch on the environment's dashboard card (owners, admins and roles that can change sign-in settings). Turn it off when anyone outside your team could guess a+testaddress there, such as a staging environment with real staff accounts; from then on those addresses get real mail and real codes, like any other, and each change is in the environment's audit log. - Redirects to localhost and
http://webhook endpoints are accepted. Stripe takes test keys only (sk_test_,pk_test_). - The hosted sign-in pages carry a ribbon with the environment's name.
Promoting changes
Build email templates, workflows, plans and plan groups, and unsubscribe groups, in an environment, then move them:
npx buildbase env diff Staging # what would change, item by item
npx buildbase env push Staging # promote (--only kind:key)
npx buildbase env pull Staging # production into StagingIn the console, Compare and promote on an environment opens Changes with the same diff. A workflow arrives as a draft and a plan as a new draft version with no Stripe ids, its prices made when it is published there; a template or a group changes in place. Users, records, history, secrets, keys and webhook endpoints never move: an HTTP step keeps the address and headers set where it lands, and a new one arrives without them. A workflow already live there takes its flow as a draft and its name in place; a different trigger, frequency cap or run limit shows as blocked, to change there or to match first. So does a different freemium setting, payer or lapse policy on a plan or group with a version published there. push and pull show the diff and ask before they change anything. --yes skips the question where there is no terminal, and --only kind:key moves one item.
From a coding agent
A coding agent such as Claude Code or Cursor can list and add environments for you. npx buildbase blueprint mcp, the blueprint MCP server, gives it two tools. environment_list lists your environments with their keys. environment_create adds one and points the app's .env.local at it. They act as the member buildbase login signed in. The client secret goes to .env.local, never into the conversation with the agent.
Plans and limits
Your plan sets how many environments you can have besides production; the console shows how many you have used. Environments are never billed: their users, usage and storage do not count toward your plan's quotas or monthly active users. Every environment gets the same allowance from BuildBase whatever your plan, shown on its dashboard as Limits in this environment. Renaming an environment changes no key, database or deployed value. Archiving one frees its place.
When a request is refused
Each refusal about an environment carries a code. Branch on the code, not on the wording of the message. The full list, with statuses, is in Error codes. The ones you meet first:
| Code | When |
|---|---|
ENVIRONMENT_NOT_FOUND | The key is not one of the organization's environments, or the environment was archived |
ENVIRONMENT_NOT_ALLOWED | The member has no access to that environment |
ENVIRONMENT_KEY_MODE | A live Stripe key saved outside production. Environments take test keys only |
ENVIRONMENT_UNSUPPORTED | A custom sign-in domain or a sending domain added outside production. Both are set up in production only |
Next steps
- Environments in the console - the switcher, who may enter, the inbox and the readiness checklist
- The buildbase CLI - every command and flag,
login,init --devandenvincluded - Going to production - what to configure before real users sign in
- Webhooks - endpoints and signing secrets, per environment
- AI agents - let a coding agent list and create environments