Environment variables and secrets
Server code reads configuration from ctx.env. Values are stored per project and per environment,
encrypted at rest, and injected into your deployed app. Nothing is baked into your build artifact, and
nothing reaches the browser.
xeer env set STRIPE_SECRET_KEY sk_live_… # production, by default
printf %s "$SECRET" | xeer env set STRIPE_SECRET_KEY # read from stdin instead
xeer env set API_BASE_URL http://localhost:4000 --environment dev
xeer env ls # names and metadata; never values
xeer env rm STRIPE_SECRET_KEY
xeer env pull # write the dev values locally#Three environments
--environment | Used by |
|---|---|
prod (default for set, rm, ls) | your production app |
preview | a preview deployment |
dev (default for pull) | xeer dev and xeer preview on your machine |
They are separate stores. A sandbox key in dev and preview and a live key in prod is the ordinary
arrangement, and it is why promoting a preview carries code forward but
never configuration — a preview's own values can never reach production by being promoted.
Every subcommand also accepts --dir <directory> to target a project other than the current one, and
--json.
#Reading a value
import { defineServer, mutation } from '@impetik/xeer/server';
export default defineServer({
mutations: {
'billing.charge': mutation({
input: { amount: 'number' },
handler: async (ctx, input) => {
const key = ctx.env.STRIPE_SECRET_KEY;
if (!key) throw new Error('STRIPE_SECRET_KEY is not set for this environment.');
// …
return { charged: input.amount };
},
}),
},
});ctx.env is Readonly<Record<string, string | undefined>>. An unset variable is undefined — never the
empty string — so a handler has to decide what to do about it rather than silently proceeding with ''.
It exposes this project's own values and nothing else; platform bindings are unreachable through it.
#Why a key cannot leak to the browser
ctx exists only in the server graph, and the client graph cannot import @impetik/xeer/server.
The compiler refuses it (XE1202), and refuses importing your server entrypoint from client code too
(XE1201). There is no import path from a component to the code that can read a secret, so there is no
bundle in which one can appear by mistake. See the two
zones.
#Local development
xeer env pullWrites the dev values to .xeer/env.dev.local.json with 0600 permissions, and makes sure .xeer/ is
in your .gitignore first. xeer dev and xeer preview load that file and inject it exactly the way a
deployment does, so ctx.env has the same shape locally and in production. Like a .env file, editing it
takes effect on the next xeer dev.
A value you only ever need locally can live in the dev store and nowhere else — there is no requirement
to mirror names across environments.
#Deployed values need a redeploy
Environment values are bindings on your deployed app, so a running app keeps the values it was deployed
with. After changing a prod or preview value:
xeer env set STRIPE_SECRET_KEY sk_live_…
xeer deployxeer env set and xeer env rm say so, and a successful deploy reports how many values it injected — so
you can tell that the change actually took effect rather than assuming it.
A rollback injects the values as they are now, not as they were when the artifact first shipped. Secrets get rotated for a reason; a rollback that restored withdrawn credentials would be a security regression dressed up as a repair.
#Values are write-only
xeer env ls shows names, environments, sizes, and timestamps. It does not show values, and neither does
anything else except xeer env pull — which writes them to a 0600 file on your machine and is audited.
ENVIRONMENT NAME BYTES UPDATED
production STRIPE_SECRET_KEY 107 2026-07-25T09:14:02.118Z
dev API_BASE_URL 27 2026-07-24T16:02:55.004ZThere is a browser dashboard for the same store, signed in with the account you use for xeer auth login.
It can add and replace a value and can never display one, because it only ever reads the same
metadata listing shown above. Every change there goes through the same endpoints the CLI calls, lands in
the same audit trail, and needs the same xeer deploy to reach a running app.
#Rules and limits
| Name | UPPER_SNAKE_CASE: an uppercase letter, then up to 62 uppercase letters, digits, or underscores |
| Reserved | names beginning XEER_ — the platform's own binding namespace |
| Value size | up to 4096 bytes |
| Count | up to 64 variables per environment, per project |
A name or value outside these is refused when you set it, with the rule in the message.
#Next
- Deploy, preview, promote, roll back — when values are injected.
- Server functions and the database — the rest of
ctx. - CLI reference — every
xeer envoption.