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.
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:
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 |
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-crmThree behaviors worth knowing beyond the table:
hs-x secretsis the one surface with an environment variable fallback:HSX_ENVIRONMENTfills the value when the flag is absent. No other command reads it.hs-x flagsresolves 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 rollbackchecks 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:
hs-x deployresolves the environment (flag, elseproduction) and bakes it into the generated Worker entrypoint, alongside the deploy id and manifest hash.- 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.
- The Worker's attestation payload carries
environmentas part of its identity, and the project's drift snapshot stores it. - 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:
$ 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.000ZRollback 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 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_requiredwhile the production install staysactive. - 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.
--envchanges identity and state keys; choosing where a deploy lands is--account-idandhs-x accounts switch. Mapping HubSpot's account types (developer test account, sandboxes, production portal) onto environments is the environments guide's subject. - An
envsblock inhsx.config.ts. The config file has no environment map, and there is no promote-by-environment-name surface;hs-x promotetakes a deploy id and the environment follows from attestation, per the deploy lifecycle reference. - Isolated compute. Worker names are environment-independent (§04); two environments of one project share one set of scripts.
- The local dev loop.
hs-x devtakes no--env. Thedevliteral labels deployed installations; the dev server is its own surface, covered in the dev-mode guide. - Invocation telemetry. The checkpoint stream behind
hs-x logsis 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.