Environment variables
Every variable the CLI and SDK read. All are optional — the defaults in the right column are what you get when the variable is unset. Authentication (see Signing in & deploying for the full precedence ladder)xanosdk preflight — its own target, deliberately separate from the deploy login
Output & diagnostics
Update notifier
Xano Engine (see Deploying locally)
Escape hatches
process.envread inside a workspace definition is a different thing entirely: it resolves at export time and bakes the literal into the bundle. For a value the deployed stack reads at runtime, useworkspaceConfig({ env })+env("NAME"), covered in the next section.
Workspace environment variables
Everything above is read by the CLI, on your machine. A workspace env var is a different set entirely: it lives in the backend, the deployed stack reads it at request time withenv("NAME"), and the CLI only ever carries it to the engine. Nothing in the table above
affects one.
Names are declared in source; values are not.
xano/ is the scaffold’s backend directory. The file is read from the directory of the
entry you compile: an entry anywhere under a top-level <dir>/ (src/index.ts, backend/index.ts) reads
<dir>/.env, and an entry at the project root reads xano/.env. A tree generate wrote
reads the .env beside its own index.ts. The info line each compile prints names the file
it looked for.
Filling it
workspace, ephemeral[:<name>],
local[:<name>], or tenant:<name> — the backend grammar
every command shares. Left off, it is the ephemeral or Xano Engine this project last
deployed to. A bundle path is refused, because its env is whatever it was built with,
which is the stale set this command exists to replace.
env pull overwrites an existing xano/.env, so it confirms first and names the source it is
about to read. A developer expecting dev secrets should not silently take production’s. --yes
skips that confirmation, including the list of local values the pull would drop. Creating a
missing xano/.env replaces nothing, so it is not asked about; a file already holding exactly
the source’s values is up to date, and nothing is asked or written.
It is the only command that writes xano/.env, but not the only place a secret lands on
disk: signing in stores your tokens in the credential file, and a compiled bundle carries the
resolved env values in cleartext (see the --bundle note below).
Changing one value on a running backend
env set creates the variable or replaces its value, and env unset removes it. Every other
variable on the backend stays as it is, and nothing is deployed. Use these to rotate a secret:
a deploy can’t do that job. A merge deploy only adds names that aren’t there yet, and
--replace rewrites the whole workspace.
The destination is --to workspace, --to ephemeral[:<name>], --to tenant:<name>, or
--to local[:<name>] for a Xano Engine. With none of these, the command writes to
the ephemeral or Xano Engine this project last deployed to. It never falls back to your
workspace or a tenant, and a Xano Engine needs no Xano account. A write to
the workspace or a tenant asks for confirmation first, and --yes skips it; that flag is
required when there is no terminal, including when the value is piped in. Ephemerals and the
Xano Engine are throwaway, so the command doesn’t ask.
The value is never printed. Pipe it on stdin to keep it out of shell history. One trailing
newline is dropped, so echo works too. An empty value is refused unless
--allow-empty-env=NAME names that variable, because an empty value usually means a shell
variable wasn’t set. To remove a variable, use env unset. unset on a name the backend
doesn’t have reports that nothing was there and exits zero.
Neither command touches xano/.env. The next deploy still sends what that file holds, so
update it too if the change should outlive the next deploy.
Overriding it, on any command that compiles a bundle:
A deploy replaces the workspace’s env set. So a declared name left empty in the config
that no file and no flag supplies would clear the live value, and
deploy refuses instead,
naming the variables. That refusal fires on the ordinary path (a fresh clone has no
xano/.env), and it is a refusal rather than a warning because the cleared value is not
recoverable from anything the project holds. A name the config itself spells a real value for
is supplied by the config and is never refused. export and preflight report instead of
refusing: one writes a file, the other imports into a throwaway tenant. A --keep-data deploy
merges, and a merge only adds names the target does not hold, so an empty value clears nothing
and is not refused — unless --keep-data cannot keep that environment’s data and the deploy
replaces it after all. A deploy --to without --replace merges the same add-only way, and it
still refuses: a name the target does not hold would be created there with no value, on a
backend someone keeps. A --dry-run sends nothing, so it reports the refusal the real deploy
would make as a warning and prints its plan. xanosdk env pull warns about each declared name
the backend holds no value for, since the file it writes has none for it either.
The refusal only covers declared names. A variable that exists in the backend but is not
in workspaceConfig({ env }), and is not among the values being sent, is dropped by the same
replacement with no refusal and no warning, because nothing declares it to check. Two ordinary
routes reach that state: someone adds a variable in the backend after your last pull, and
swapping xano/.env for an --backend-env-file that holds only the declared names. So declare every
name the stack reads, and carry the rest across when you swap files.
The refusal is part of the compile, so xanosdk deploy --bundle <path> does not get it. A
pre-built bundle carries whatever env it was built with and sends exactly that, so a
build-once-deploy-the-artifact pipeline resolves its values at build time: pass
--backend-env-file/--env-var to the export that produces the bundle. Two consequences worth
knowing before you build that pipeline:
- The bundle is a secret-bearing file. A compiled bundle carries the resolved values in
cleartext. Keep it out of git and out of shared build artifacts, and delete it after the
deploy. The scaffold gitignores
workspace.json, but not an arbitrary--outpath. - Nothing re-checks the env at that deploy.
--env-var,--backend-env-file, and--allow-empty-envare accepted on a--bundledeploy and do nothing, because there is no compile to resolve them into. Rebuild the bundle instead, and rebuild it whenever a declared name is added: theexportthat skipped a value only warns, and the deploy that ships it does not check again.
xano/.env is backend-only. The token gating
a hosted doc site lives in xano/.secrets.json, also gitignored, also preserved across a pull.
The split is not arbitrary, and the rule tells you where a future secret lands. A backend
variable is addressed by name — env("NAME") is how a stack reads it, so the name is its
identity, and a dotenv file is the right shape. A documentation token is addressed by the
object that holds it: this workspace, that API group. It has no name of its own, so it is
stored under that object’s identity instead, and env(...) cannot read it from anywhere.
That file is SDK-owned. xanosdk pull writes it and every build reads it back, so a pull
followed by a deploy restores the gate with no manual step — which is the whole reason it
exists. Nobody edits it by hand; --no-secrets on a pull finds the tokens and writes none, for
a machine where a secret on disk is not wanted.
A pull only answers for an object that already HAS a token. A gate you declare in source — a new
API group, or a doc site you decided to close — has none to fetch, and that is what
xanosdk secrets fill is for:
--secrets-file <path> (which replaces the default, the
way --backend-env-file does) or --doc-token "<scope>=<value>" for one scope. A scope is workspace
or an API group’s name; the flags take one per occurrence and repeat, and refuse a name two
groups answer to rather than guessing which you meant. Prefer the file: a token on the command
line is visible in shell history, the process list, and any CI log that echoes the command.
A pull refuses to write the file when git says the path is not ignored, and reports the drop
rather than failing — the tree it wrote is fine, it just carries no tokens. When git cannot be
asked at all (no repository, no git on PATH) it writes: there is nothing there that could
commit the secret.
The refusal works the same way as the backend one, with one difference worth knowing: an
unsupplied gate emits no documentation block at all rather than an empty token. On the
workspace that leaves the target’s gate as it is instead of clearing it, which is what stops
a two-step export then deploy --bundle pipeline from clearing a gate no refusal could fire
on.
On an API group an absent key is not a non-action — the engine writes its default, which
clears that group’s gate. There is no safe bundle to write, so a group that publishes docs and
declares a gate with no token supplied fails the export on every command, not just
deploy. Only --allow-empty-doc-token=<scope> turns an unsupplied gate into a deliberately
cleared one.
One thing to clear out by hand if you used an earlier build: a XANO_DOCS_TOKEN… line left in
xano/.env from when tokens lived there is now an ordinary backend variable, because that file
is backend-only again. It will be sent as part of the workspace’s env, where env("NAME") reads it
from every request. Delete the line — the value belongs in xano/.secrets.json, and xanosdk pull
puts it there.
An entry the file holds that no scope in your source claims is reported and never deleted —
usually a renamed API group in a project with no lock. The SDK cannot tell a rename from a group
you commented out, and it cannot re-fetch a secret it throws away.
Do not confuse any of this with deploy --static-env, which bakes public config into the
HTML a browser downloads. One is a workspace secret; the other is visible to everyone.
typedEnv(), the stack.env-undeclared warning, and what a release does with env across
environments are in Authoring reference.
Identity & the xano.lock file
Every top-level object carries a stableguid — Xano’s identity anchor. On a sync import
the engine matches an incoming object to an existing one by guid and updates it in
place; no match means a new object. So re-running export/deploy on the same code maps
cleanly onto the same workspace — no duplicates. By default the guid derives from the
object’s name — for a query, from its api group, verb, and name together, which is
the engine’s own uniqueness for an endpoint, so GET items and POST items are distinct
objects and a path may repeat across groups. Set an explicit guid to pin identity across
a rename, or to adopt an existing workspace object into code.
xano.lock freezes the whole workspace’s identities at once — every auto-derived guid,
plus the canonical URL tokens of API groups and toolsets (which the engine otherwise
randomizes, giving the same code different public URLs per environment). Every command that
compiles an entry file writes it — export, deploy, release, preflight, with no flag —
and updates it on every build, atomically, before the bundle. Commit it next to your code.
npm run xano:check is the CI guard (xanosdk export ./xano/index.ts --check --strict, which implies
--frozen-lock, writes nothing, and needs neither xano/.env nor xano/.secrets.json): it fails rather than change the lock,
so an uncommitted identity change is caught in review instead of on a deploy. It also fails
while the lock carries an entry no exported object matches. That entry is how a rename looks
before its fix-up — the old name keeps the identity while the new name mints a fresh one, so
the next sync deletes the object and creates another in its place. A plain export only warns
and writes the entry down, and from then on nothing tells a rename from a deletion, so the
check asks you to say which: lock rename to finish the rename, lock prune to drop an entry
whose object really is gone. If you adopted a live workspace with lock import and are porting it
a piece at a time, those unported entries pin objects that are still serving and must not be
pruned — pass --allow-lock-orphans to accept them and keep the rest of the check. An installed
toolchain module’s own frozen check rides on the same run, so a stale artifact it owns fails
there too, instead of being rewritten.
compile and paths/routes only read a lock; they never write one, so listing your
routes never mints a URL token it would then discard.
--no-lock builds without one, for a throwaway run or a tree that cannot be written.
Identities then derive from names, the instance invents public URLs your project will not
learn, and deploy --to workspace --prune is unavailable. It is refused over a lock that already
parses — building name-derived guids against a lock that pins different ones is a
delete-and-recreate of every object, reported as success. If the lock itself is corrupt,
--no-lock is the way past it.
Releasing to a workspace that already exists? Adopt what it already serves first:
canonical in code. The workspace keeps the slug it already serves — an unpinned
value is a preference, and an update keeps the stored one — so the lock would record a URL
that is not the one being served, and would keep recording it. Adopting first makes the lock
true. A brand-new workspace has nothing to adopt and needs no such step.
One directory, one lock. The lock lands beside the entry file, so two entry files in
the same directory share it — each build then reports the other’s objects as orphans, and a
lock prune would delete them. Give each entry its own directory, or name its lock
explicitly with --lock=<path>.
Precedence at emit is always explicit in-code value → lock entry → name derivation.
When the lock is actually load-bearing. Because the default derivation is deterministic
— md5("<type>:<name>"), and md5("query:<group>|<verb>|<name>") for a query — a project
that created all of its own objects can regenerate a
byte-identical lock from its own source. Delete that lock, release again, and the same guids
come back: the objects match and update in place. For that project the lock is a cache, and
losing it costs nothing.
The lock is load-bearing exactly where a live guid diverges from that derivation, which
happens two ways:
- Adopted objects — anything built in the Xano UI first and taken over with
lock import. The engine assigned those guids randomly; nothing in your code can re-derive them. - Renamed objects —
lock renamepins the original guid under the new name, so the derivation no longer reproduces it.
<<<<<<< markers is refused by every command, saying so. Resolve it as
the union of both sides: keep every entry under objects (and landed) from each, then
delete the markers. If both branches renamed the same object — one to stock, the other to
goods — the union holds one guid under two keys, which every command refuses, naming both.
Keep the name your code exports and drop the other; both commands read such a lock:
rename/adopt take no entry file, so on their own they look for xano.lock in the
current directory. Pass --entry=<path> to derive it beside the entry the way
export/deploy/prune do, or --lock=<path> to name the file outright. They never
reach for a lock you did not point them at — when they spot one next door they say so and
stop, rather than writing a file you did not name.
After --replace — a replace rebuilds the workspace with fresh engine identities, so the
lock is stale the moment it finishes and the next ordinary merge would match nothing and
try to create everything. xanosdk deploy ./xano/index.ts --to workspace --replace now re-pins the lock from the rebuilt
workspace itself and tells you to commit it. If there is no lock to re-pin, it says so —
without one, the next merge duplicates every object.
Importing a live workspace’s identities — xanosdk lock import <bundle.json> seeds the lock from a
real engine packageExport, capturing the live workspace’s random guids by (type, name)
so code takes over an existing workspace and the first sync updates in place instead of
duplicating.
CI — xanosdk export ./xano/index.ts --check --strict (the scaffolded npm run xano:check) runs every
check an export runs and writes nothing, so it needs no secrets. It implies --frozen-lock, which
fails instead of changing the lock, so a canonical
minted in a throwaway container can never silently diverge public URLs. Mint locally, commit
the lock. It fails on an entry no exported object matches too, which is the state a rename
leaves behind until lock rename moves the identity across (or lock prune drops it).
Generated from @xano/sdk 1.0.9.