> ## Documentation Index
> Fetch the complete documentation index at: https://biznetgio.creations.ren/llms.txt
> Use this file to discover all available pages before exploring further.

# Environments: dev, staging, and production

> Keep environments isolated with Terraform workspaces and Pulumi stacks

One set of configuration files, run against several separate environments, each with its own state and its own settings. That's the whole idea: the same `main.tf` or `index.ts` from the [project structure](/tutorials/project-structure) tutorial, applied three times with three different answers to "how big" and "who pays."

## Terraform workspaces

A workspace is an isolated slot of state within the same backend. Every workspace shares the same configuration files.

```bash theme={null}
terraform workspace new staging
terraform workspace new production
terraform workspace list
terraform workspace select staging
```

Reference the current workspace inside your configuration with `terraform.workspace`. The `data` blocks and the `biznetgio_neolite_keypair` resource are exactly what the [quickstart](/quickstart) already declared in full - what's different here is that `product_id` is no longer `products[0]`. It's picked deliberately per workspace, using the real package names from the [NEO Lite catalog](/products/neolite):

```hcl theme={null}
data "biznetgio_neolite_products" "all" {}

locals {
  # Real NEO Lite package names - see /products/neolite for the full pricing table.
  # dev gets the cheapest tier on purpose; staging matches production so it
  # actually catches production-shaped bugs.
  tier_by_workspace = {
    dev        = "XS 1.1" # 1 vCPU / 1 GB - enough to smoke-test a plan/apply cycle
    staging    = "MS 4.2" # 2 vCPU / 4 GB - same tier as production
    production = "MS 4.2"
  }

  matching_products = [
    for p in data.biznetgio_neolite_products.all.products :
    p if p.name == local.tier_by_workspace[terraform.workspace]
  ]

  # Errors at plan time if the name above doesn't match anything on your
  # account, instead of silently falling back to whatever products[0] is.
  selected_product = local.matching_products[0]
}

data "biznetgio_neolite_os_list" "ubuntu" {
  product_id = local.selected_product.product_id
}

resource "biznetgio_neolite_vm" "main" {
  vm_name              = "web-${terraform.workspace}"
  product_id           = local.selected_product.product_id
  select_os            = data.biznetgio_neolite_os_list.ubuntu.oss[0].name
  keypair_id           = biznetgio_neolite_keypair.main.keypair_id
  cycle                = "m"
  ssh_and_console_user = "adminuser"
  console_password     = var.console_password

  # cheap and reversible while testing, real money in production
  pay_with_credit_card = terraform.workspace == "production"
}
```

<Note>
  Confirm `"XS 1.1"` and `"MS 4.2"` against your own account's catalog output before relying on them - see [Step 1 in the catalog overview](/products/overview#step-1-look-at-your-own-catalog-before-you-filter-anything). `select_os` still uses `oss[0]` here since OS lists are usually short and dominated by one obvious choice; apply the same name-filter pattern to it if that's not true for your account.
</Note>

<Warning>
  Always check which workspace is selected before applying: `terraform workspace show`. Applying to the wrong workspace is the single most common way people accidentally touch production. Consider wrapping `apply` in a script or CI job that requires the workspace name as an explicit argument instead of trusting whatever is currently selected.
</Warning>

If dev, staging, and production ever need genuinely different resources (not just different values), workspaces stop being the right tool, since all workspaces share one configuration. At that point, move to a directory per environment (`envs/dev`, `envs/staging`, `envs/production`), each with its own backend configuration and `tfvars`, all calling the same shared [module](/tutorials/modules-and-components).

## Pulumi stacks

A stack is Pulumi's native equivalent, and it's the primitive the quickstart already used without naming it.

```bash theme={null}
pulumi stack init dev
pulumi stack init staging
pulumi stack init production
pulumi stack ls
pulumi stack select staging
```

Each stack gets its own `Pulumi.<stack>.yaml` config file and independent state. Set config per stack explicitly with `--stack`:

```bash theme={null}
pulumi config set --stack dev biznetgio:apiToken <dev-token> --secret
pulumi config set --stack production biznetgio:apiToken <production-token> --secret
```

Read the current stack's name inside your program with `pulumi.getStack()`. `keypair` and `config` are exactly what the [Pulumi quickstart](/pulumi-quickstart) already declared in full - what's different here is that `productId` is no longer `products[0]`. It's picked deliberately per stack, using the real package names from the [NEO Lite catalog](/products/neolite):

```typescript theme={null}
import * as pulumi from "@pulumi/pulumi";
import * as biznetgio from "@shirasakaren/biznetgio";

const stack = pulumi.getStack();
const products = biznetgio.neoliteProductsOutput();

// Real NEO Lite package names - see /products/neolite for the full pricing table.
// dev gets the cheapest tier on purpose; staging matches production so it
// actually catches production-shaped bugs.
const tierByStack: Record<string, string> = {
  dev: "XS 1.1", // 1 vCPU / 1 GB - enough to smoke-test a preview/up cycle
  staging: "MS 4.2", // 2 vCPU / 4 GB - same tier as production
  production: "MS 4.2",
};

const productId = products.products.apply((items) => {
  const match = items.find((p) => p.name === tierByStack[stack]);
  if (!match) throw new Error(`no NEO Lite product named '${tierByStack[stack]}' found`);
  return match.productId;
});

const osList = biznetgio.neoliteOsListOutput({ productId: productId });

const vm = new biznetgio.NeoliteVm("main", {
  vmName: `web-${stack}`,
  productId: productId,
  selectOs: osList.oss[0].name,
  keypairId: keypair.keypairId,
  cycle: "m",
  sshAndConsoleUser: "adminuser",
  consolePassword: config.requireSecret("consolePassword"),
  payWithCreditCard: stack === "production",
});
```

<Note>
  Confirm `"XS 1.1"` and `"MS 4.2"` against your own account's catalog output before relying on them - see [Step 1 in the catalog overview](/products/overview#step-1-look-at-your-own-catalog-before-you-filter-anything). `selectOs` still uses `oss[0]` here since OS lists are usually short and dominated by one obvious choice; apply the same name-filter pattern to it if that's not true for your account.
</Note>

<Warning>
  Same rule as Terraform: check `pulumi stack ls` (the current one is marked) before `pulumi up`. Pass `--stack <name>` explicitly in CI rather than relying on whichever stack was last selected on that runner.
</Warning>

## A practical dev/staging/production split

A pattern that works well for these providers specifically:

| Environment | `pay_with_credit_card` / `payWithCreditCard` | Product tier            | Purpose                                                                           |
| ----------- | -------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------- |
| dev         | `false`                                      | cheapest available      | validate plans/previews without a real charge; resource sits `Pending` until paid |
| staging     | `true`                                       | same tier as production | catch real-world behavior before it matters                                       |
| production  | `true`                                       | the real tier           | what customers actually use                                                       |

Read the [billing guide](/guides/billing) for exactly what `pay_with_credit_card = false` does; it's the cheapest way to rehearse a whole apply/up cycle before committing real money.

## Next steps

* [State, remote backends, and team collaboration](/tutorials/state-and-collaboration) - get each environment's state off your laptop
