# 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.

> **Note:**
  Sign the CLI in to your console once: `npx buildbase login`. Adding an
  environment needs an owner or admin.


> **Note:**

**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

```bash
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 one
```

`init --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:

```tsx
<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}
>
  
</SaaSOSProvider>
```

```ts
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](/reference/mcp-agents), 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`:

```ts title="app/api/auth/verify/route.ts"
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` (`jane+test@example.com`) 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 `+test` address 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:

```bash
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 Staging
```

In 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](/blueprints/ai-agents), 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](/reference/error-codes#environments). 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](/quick-start/environments-console) - the switcher, who may enter, the inbox and the readiness checklist
- [The buildbase CLI](/reference/cli) - every command and flag, `login`, `init --dev` and `env` included
- [Going to production](/quick-start/production) - what to configure before real users sign in
- [Webhooks](/webhooks/overview) - endpoints and signing secrets, per environment
- [AI agents](/blueprints/ai-agents) - let a coding agent list and create environments
