view .md
Reference · Platform

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.

Time
≈ 7 min read
Outcome
A precise model of what --env changes: the installation-identity tuple, the per-environment state stores and route pointer, and the resources every environment of a project 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:

DimensionWhat it identifies
accountIdThe HS-X account that owns the project
projectIdThe project
environmentdev, staging, or production
hubSpotAppIdThe HubSpot app the install came through
portalIdThe 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

CommandFlagDefaultWhat the environment selects
hs-x deploy--env (also --environment)productionThe 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|devproductionWhich 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)productionWhich per-environment signing credentials and scope the CLI loads from local state; flag definitions live per environment
hs-x rollback (unlinked)--environment (also --env)productionWhich environment's active pointer moves
hs-x promotenonen/aThe promoted environment is read from the deploy's healthy attestation, never from a flag
hs-x routesnonen/aReads 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-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:

$ 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:

ResourceName shapePer-environment
Worker scripthsx-<account>-<project>-<worker>No
Worker-plan DO / KV / D1 / queue / cronhsx-<account>-<project>-<worker>-do (-kv, -d1, -queue, -cron)No
Install token KVhsx-<account>-<project>-<environment>-<appId>-installYes
Tenant D1 (linked deploys)hsx-<account>-<project>-<environment>-<appId>-tenantYes
Flags snapshot KV (linked deploys)hsx-<account>-<project>-<environment>-<appId>-flagsYes

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_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'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.
  • 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.
  • 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.