Environments
Separate copies of one project, and the promotion that moves configuration between them.
An environment is a full copy of a project — its own users, sessions,
identities, providers, templates and keys. Nothing is shared. A user created in
develop does not exist in production, and never will, because they were never
in that store.
What does move between environments is configuration, and only when you promote it deliberately.
One project, several issuers
Each environment answers on its own issuer URL, derived from the project slug:
organization
└── project · acme
├── environment · main → acme.nulvless.co
├── environment · staging → acme-staging.nulvless.co
└── environment · develop → acme-develop.nulvless.coProduction's slug is main, and it is the one environment whose URL carries
no suffix. The console displays it as "Production"; every other environment
shows its raw slug, which is why the same environment can read Develop in a
picker and develop in a sentence.
The issuer URL is the first argument to createClient. Pointing an app at
another environment is a URL change and a key change — not a code change. See
Clients.
Creating one
From the environment picker in the console header, or from Others → Environments. An environment takes a name and a slug:
namestringRequiredThe label you read in the console. Free text.
slugstringRequiredThe identifier in the URL, and what the merge screens address it by. Choosing
staging gives you acme-staging.nulvless.co.
A new environment builds before it can serve traffic:
buildingstatusBeing provisioned. Not yet usable.
readystatusServing on its issuer URL.
errorstatusProvisioning failed.
Keys are per environment, so a fresh environment starts with its own publishable and secret keys. Nothing you configured elsewhere comes with it — which is the point, and also the first surprise.
Comparing before merging
Promotion starts as a diff, not a write. The Compare screen takes an origin and a destination and shows every setting that differs:
Compare environments
What would change if you promoted development to production.
Each row is one setting, with the destination's current value on the left and
the incoming value on the right. The arrow points at the destination, and the
sentence underneath the picker spells it out:
"37 change(s) will be merged from Production into Develop." The confirmation
dialog labels the two sides ORIGIN and DESTINATION outright.
The field on the left is the destination — the environment that will be written to. Read the sentence, not the layout, before you apply anything.
Underneath, the diff is exactly two sections — auth and communications —
each a list of field, before, after. The console fans them out into
readable groups (sign-up, sign-in methods, password, MFA, tokens, social
providers, sudo mode, attack protection, OAuth server, rate limits,
notifications), but that grouping is presentation. The unit that moves is a
settings field.
What a merge does not carry
This is the part worth internalising, because the omissions are deliberate and none of them are errors:
credentials and secretsnever mergedProvider API keys, SMTP passwords, OAuth client secrets. They must be configured by hand in the destination. The console warns about this on the Compare screen for good reason: a promoted provider that is enabled but unconfigured fails at delivery time, not at merge time.
users, sessions, identitiesnever mergedEnvironments are separate stores. Promotion moves settings, never data.
edge functionsnot environment-scopedA function belongs to the project, not to an environment, so there is nothing
about it in a diff. Its URL is always built from the production slug —
acme.nulvless.co/functions/<slug>.
Applying it
Merging is sudo-protected. Confirming opens the access challenge, and the confirmation holds for one hour — so a sequence of promotions asks once, not every time.
A merge cannot be undone. There is no revert, and the destination's previous
values are not kept as a restore point — only as the before column of the
recorded snapshot. Promote into develop or staging first and read the diff
there.
The merge itself runs as a pipeline and produces a deployment:
queuedstatusAccepted, not started.
runningstatusSteps in progress.
succeededstatusEvery step finished.
failedstatusA step failed. The step list shows which.
A run promotes the two sections in order — initialise, then authentication, then communications — and the detail screen times each step individually. A real one completes in about three seconds.
Reading the history
Others → Environments lists every environment and, below them, Environment merges — the history of promotions, filterable by status. Each row opens a detail screen carrying the origin, the destination, the deployment id, who requested it, a timestamped event log, and an Applied changes snapshot: the settings as they were compared when the merge was requested.
That snapshot is the audit trail: the same before-and-after rows you reviewed, frozen at the moment the merge was requested. It is what answers "who changed this, and what was it before" months later, without depending on anyone having written it down.
A merge description is generated for you when the diff is prepared, so the history reads as prose rather than a field count. It is written from the diff at request time — it describes what was promoted, not what the environment looks like now.
A working shape
Promote in one direction and let the diff stay small:
Change settings in develop and exercise them against
acme-develop.nulvless.co.
Compare develop into staging. Read the diff — it is the review.
Configure the destination's credentials by hand, once, before the first promotion that needs them.
Promote into main last, when the same configuration has already run somewhere
else.
A large diff is not a merge problem; it is a sign that two environments have been edited independently for a while. The cheapest fix is to promote more often.