# Environments.

An HS-X environment is one of three literal values (`dev`, `staging`, or `production`) baked into a deployed Worker and carried as the third dimension of installed-portal identity. Secrets, flag definitions, install token stores, tenant databases, billing entitlements, and the active-deploy route are all keyed by it; Cloudflare Worker names are not. This page is the exact contract: the allowed values and defaults, which commands accept the flag, what state each environment isolates, and what every environment of a project still shares.

## The 30-second answer

Every environment-aware surface in HS-X accepts `dev`, `staging`, or `production`, and the default is **`production`** everywhere a value can be omitted. An environment is an axis of identity rather than a separate copy of your project: installed portals, OAuth client secrets, sealed install tokens, flag definitions, billing entitlements, and the active-deploy route are all keyed by `(account, project, environment, …)`, while the project's deploy history and its Cloudflare Worker scripts carry no environment at all.

`hs-x deploy --env staging` bakes `staging` into the generated Worker. From there the value travels on its own: the Worker attests it to the control plane, the drift snapshot records it, and promotion writes the staging route entry. The same deploy run also provisions staging-named state stores and reads the staging OAuth client secret.

**If you only read one thing**

**Environments isolate state and identity, not compute.** A project's Worker names contain no environment segment, so deploying a second environment overwrites the same scripts. Everything keyed by environment (tokens, secrets, flags, billing, routes, installed portals) stays separate.

## The third axis of installation identity

The canonical identity of an installed portal is a 5-tuple, and the environment sits in the middle of it:

| Dimension | What it identifies |
| --- | --- |
| `accountId` | The HS-X account that owns the project |
| `projectId` | The project |
| `environment` | `dev`, `staging`, or `production` |
| `hubSpotAppId` | The HubSpot app the install came through |
| `portalId` | The customer portal that installed |

Every record that describes an installation is addressed by this full tuple: the installed-portal lifecycle row, billing customers, usage, and entitlements, flag exposure records, and platform-event occurrences. An `installId` rides alongside the tuple as a runtime lifecycle id for traceability, but it is not part of the key. Change any one dimension and you are looking at a different installation; the environment is a dimension like the others.

The first three dimensions form the ownership spine the rest of this page keys on — one HS-X account owns the project, and the project's environments resolve to the HubSpot portals and Cloudflare account a deploy actually lands on:

[figure]

**An environment in HS-X is one of exactly three values: `dev`, `staging`, or `production`, and `production` is the default on every surface that takes one.** The set is closed. `hs-x secrets` validates the value up front as a choice; the other commands accept text, and anything outside the three literals fails at the schema layer downstream: an unlinked deploy cannot write its tenant record, and a linked Worker's attestation is refused with a 400, which means the deploy can never pass the promotion gate.

## The commands that take --env

| Command | Flag | Default | What the environment selects |
| --- | --- | --- | --- |
| `hs-x deploy` | `--env` (also `--environment`) | `production` | The value baked into the generated Worker: attestation identity, the runtime billing scope, and the flag/sync grant scope. Also which per-environment OAuth client secret the deploy reads, the names of the per-app stores it provisions (§04), and, on unlinked deploys, the environment on the tenant deploy entry and active pointer |
| `hs-x secrets hubspot-oauth set` | `--env production\|staging\|dev` | `production` | Which scoped secret slot the client id and secret land in: one per (account, project, environment, app) |
| `hs-x flags list\|create\|enable\|disable\|archive` | `--environment` (also `--env`) | `production` | Which per-environment signing credentials and scope the CLI loads from local state; flag definitions live per environment |
| `hs-x rollback` (unlinked) | `--environment` (also `--env`) | `production` | Which environment's active pointer moves |
| `hs-x promote` | none | n/a | The promoted environment is read from the deploy's healthy attestation, never from a flag |
| `hs-x routes` | none | n/a | Reads all of them: one output line per environment |

```sh
hs-x deploy --env staging --promote-when-healthy
hs-x secrets hubspot-oauth set --account-id acct_demo --project-id acme-crm \
  --hubspot-app-id 123456 --client-id <id> --client-secret <secret> --env staging
hs-x flags disable --key new-pricing --project-id acme-crm --app-id 123456 \
  --env staging --runtime-origin https://<worker-url>
hs-x routes --project acme-crm
```

Three behaviors worth knowing beyond the table:

- `hs-x secrets` is the one surface with an environment variable fallback: `HSX_ENVIRONMENT` fills the value when the flag is absent. No other command reads it.
- `hs-x flags` resolves its credentials per environment, so a project deployed only to production has nothing for staging. The refusal is explicit: `No flag-authoring credentials for <project>/<environment> (app <id>). Re-deploy as a linked project to provision the tenant data plane.`
- Unlinked `hs-x rollback` checks the target's recorded environment and refuses a mismatch: `Deploy <id> is for environment staging, not production. Pass --environment staging to roll back its environment.`

## How a deploy gets its environment

The environment is not part of the deploy plan or the deploy record. It enters at code generation and travels with the running Worker:

1. `hs-x deploy` resolves the environment (flag, else `production`) and bakes it into the generated Worker entrypoint, alongside the deploy id and manifest hash.
2. The plan request and the recorded revision carry no environment. Deploy ids, history, and archived bundles are project-level. **A deploy record has no environment field; a deploy belongs to whatever environment its Worker attests.**
3. The Worker's attestation payload carries `environment` as part of its identity, and the project's drift snapshot stores it.
4. Promotion reads the snapshot: when the gate passes, the route entry for the *attested* environment is rewritten to name the new deploy. The control plane keeps exactly one route entry per environment per project.

`hs-x routes` is the read side, one line per environment:

```sh
$ hs-x routes --project acme-crm
production active=deploy_acme-crm_007 previous=deploy_acme-crm_006 reason=promote updated=2026-06-11T14:02:11.000Z
staging    active=deploy_acme-crm_009 previous=deploy_acme-crm_008 reason=promote updated=2026-06-11T16:40:03.000Z
```

Rollback fits the same model from the other side. A linked rollback finds the route entry that names the rolled-back-from deploy and rewrites that entry (falling back to `production` when no entry names it); an unlinked rollback takes `--environment` directly and moves the matching local pointer. When `hs-x link` backfills unlinked history into the control plane, each backfilled entry carries the environment it was originally deployed under.

## What is named per environment

A deploy touches two families of Cloudflare resources, and they are named differently:

| Resource | Name shape | Per-environment |
| --- | --- | --- |
| Worker script | `hsx-<account>-<project>-<worker>` | No |
| Worker-plan DO / KV / D1 / queue / cron | `hsx-<account>-<project>-<worker>-do` (`-kv`, `-d1`, `-queue`, `-cron`) | No |
| Install token KV | `hsx-<account>-<project>-<environment>-<appId>-install` | Yes |
| Tenant D1 (linked deploys) | `hsx-<account>-<project>-<environment>-<appId>-tenant` | Yes |
| Flags snapshot KV (linked deploys) | `hsx-<account>-<project>-<environment>-<appId>-flags` | Yes |

All names are lowercased slugs, and the per-app store names are truncated to Cloudflare's 63-character resource-name limit. The first family comes from the deploy plan and the generated Alchemy program; the second is provisioned during the deploy for the HubSpot app the project ships.

**Cloudflare Worker names never include the environment; the per-app state stores always do.** The consequence deserves a plain statement: a project has one set of Worker scripts, and each deploy overwrites them in place. Running `hs-x deploy --env staging` after a production deploy replaces the code production traffic hits, and the next heartbeat attests `staging`. The route table can hold a production pointer and a staging pointer at the same time, but only one environment's code is actually serving. If you need two environments live at once today, use the shape [the environments guide](/docs/guides/environments) describes: one project per target portal, with the environment label marking which is which.

## One portal, two environments, two installations

**The same app installed in the same portal under two environments is two installations.** Everything keyed by the identity tuple splits cleanly:

- The installed-portal record's primary key is the full 5-tuple, so the two installs hold independent lifecycle states. A staging install can sit in `reauth_required` while the production install stays `active`.
- Sealed OAuth install tokens live in the environment's install KV. A portal that installed through the staging deployment has tokens only in the staging store; production holds nothing for that portal until it installs again through a production deploy.
- Installer PII and platform-event occurrences land in the environment's tenant D1, in the developer's own Cloudflare account.
- Flag definitions, snapshots, and exposure records are per environment, so disabling a staging flag cannot change a production evaluation.
- Billing is addressed by the full installation identity. The runtime reports usage under its baked environment, and an entitlement created for a portal's staging install never satisfies a production entitlement check.

This separation is what the environment dimension exists for, and its cost is symmetric and intentional: nothing migrates between environments automatically. Secrets are set per environment, flags are defined per environment, and portals install per environment.

## What an environment does not select

The honest edges of the model, as shipped:

- **A HubSpot portal or Cloudflare account.** `--env` changes identity and state keys; choosing where a deploy lands is `--account-id` and `hs-x accounts switch`. Mapping HubSpot's account types (developer test account, sandboxes, production portal) onto environments is [the environments guide](/docs/guides/environments)'s subject.
- **An `envs` block in `hsx.config.ts`.** The config file has no environment map, and there is no promote-by-environment-name surface; `hs-x promote` takes a deploy id and the environment follows from attestation, per [the deploy lifecycle reference](/docs/deploy-lifecycle).
- **Isolated compute.** Worker names are environment-independent (§04); two environments of one project share one set of scripts.
- **The local dev loop.** `hs-x dev` takes no `--env`. The `dev` literal labels deployed installations; the dev server is its own surface, covered in [the dev-mode guide](/docs/guides/dev-mode).
- **Invocation telemetry.** The checkpoint stream behind `hs-x logs` is keyed by project and deploy id; environment is not a filter there. What carries the environment is identity-shaped telemetry: attestations, install lifecycle, platform events, flag exposures, and billing usage.

## Related reference

- [Environments](/docs/guides/environments) — the journey-level guide: HubSpot account types, wiring accounts into HS-X, and CI/CD per environment.
- [Deploy lifecycle](/docs/deploy-lifecycle) — the state machine this page's route entries belong to: recording, attestation, promotion, rollback.
- [Secrets](/docs/guides/secrets) — storing the per-environment OAuth client credentials a deploy injects.
- [Feature flags](/docs/guides/feature-flags) — authoring and evaluating the per-environment flag definitions referenced in §05.
- [Billing](/docs/guides/billing) — the entitlement and usage model keyed by the installation identity tuple.
- [CLI reference](/docs/cli) — the full flag surface for every command in §02.

---

*Last updated: June 11, 2026. Behavior reflects the CLI, control plane, and runtime shipped with CLI `v0.2.5`. Refreshed when the environment surface changes.*

