Environment Variables
Manage environment variables for your project. Variables are site-scoped —
each site has its own set. Commands target the active site stored in the
directory’s .ghayma.json.
Multi-site projects: every env command takes --site <slug> (slug,
name or id). Without it, env uses the site of the directory you’re in — and
at a workspace manifest root it asks
which site you mean, or fails rather than guess on a non-interactive shell.
See Sites.
A site can inherit another site’s variables. A non-production
environment resolves the project’s default
site’s variables as its base and overrides them key by key, so env list there shows rows it does not own and env delete behaves differently on
them. See Inherited variables.
Runtime vs. build-time
By default, env vars are runtime-only — they’re injected into the running
container but are not visible during the Docker build. Frameworks that
inline env vars into the client bundle at build time (Next.js NEXT_PUBLIC_*,
Vite VITE_*, CRA REACT_APP_*, Expo EXPO_PUBLIC_*) need those vars
present when next build / the bundler runs — pass --build-time to make a
var visible to the build as well.
Build-time values land in image layers and are visible to anyone with
registry pull access. The server refuses to mark likely-secret keys
(matching _SECRET, _TOKEN, _PASSWORD, PRIVATE_, CREDENTIAL) as
build-time; pass --force to override if you truly need to.
The database variables the platform
derives are the exception: every build of a connected site receives them as
build secrets, readable while the build runs and never written into the
image. A Valkey’s variables are not: they are runtime only. A connected database
needs no build-time DATABASE_URL of your own. See Using your database during
the build and Valkey.
ghayma env set
Set one or more environment variables on the active site, or on the site you
name with --site.
# One variable
ghayma env set DATABASE_URL=postgres://...
# Multiple at once
ghayma env set API_KEY=sk-123 DEBUG=true
# Build-time (visible to `next build`, etc.)
ghayma env set --build-time NEXT_PUBLIC_API_URL=https://api.example.com
# Override the secret-pattern rejection (use sparingly)
ghayma env set --build-time --force MY_API_KEY=...
# A specific site, from anywhere in a workspace
ghayma env set --site admin API_KEY=sk-123Output:
✅ Environment variables updated:
DATABASE_URL=postgres://...
API_KEY=sk-123When --build-time is used, the CLI prints a warning after the update:
⚠️ Build-time variables are embedded in image layers and visible
to anyone with registry pull access. Do not use for secrets.
🔄 Redeploy to apply: ghayma deployChanges don’t apply to the running deployment until you redeploy.
Setting a key a site inherits turns that row into the site’s own override. See Inherited variables.
ghayma env list
List all environment variables for the active site, or for the site you name
with --site. Build-time vars are tagged with a [build-time] badge.
ghayma env list
ghayma env list --site adminOutput:
🔧 Environment variables for my-project:
API_KEY=sk-123
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_URL=https://api.example.com [build-time]When no vars are set, prints No environment variables set.
On a site that inherits, the listing is what the site resolves to — its own rows plus the ones it inherits, each marked:
🔧 Environment variables for my-project:
API_TIMEOUT=30 (inherited from main)
LOG_LEVEL=debug
NEXT_PUBLIC_API_URL=https://api.example.com [build-time] (inherited from main)
2 of 3 variable(s) are inherited from 'main'. Set the same key here to override one; credentials are never inherited.ghayma env delete
Remove one or more environment variables.
ghayma env delete API_KEY
ghayma env delete FOO BAR BAZ
ghayma env delete --site admin API_KEYEach key is reported on its own line:
✅ API_KEY removed
🔄 Redeploy to apply: ghayma deployA key the site does not have is a no-op, not an error —
ℹ️ BAZ is not set on this site — no change. — which is how a typo shows up.
On a site that inherits, deleting your own row re-exposes the base site’s value instead of removing the variable, and the command says so:
✅ LOG_LEVEL: the override is gone — the value inherited from 'main' applies againDeleting a variable the site only inherits is refused — it lives on another site, and succeeding silently would tell you it is gone while every deploy kept injecting it:
❌ API_TIMEOUT: this variable is inherited from main; override it or edit it there
Override it here instead: ghayma env set API_TIMEOUT=<value>Inherited variables
A non-production environment resolves its variables
through a ladder: the project’s default site’s rows as the base, the
site’s own rows overriding them key by key. It is on by default for new
non-production sites, off for every site that existed before, and switched with
ghayma site inherit-env.
- Setting a key that is inherited makes it this site’s own override — value, build-time marking and all.
- Deleting that override brings the inherited value back.
- Deleting a purely inherited key is refused; override it here, or edit it on the base site (which changes it for production too).
- Anything you did not override keeps following the base site, so a variable added there later appears here as well.
No credential is ever inherited. DATABASE_URL, MONGODB_URI,
REDIS_URL, the STORAGE_* keys, the auth app’s variables,
CRON_SECRET_* and the managed GHAYMA_API_KEY are
derived from this site’s own connections on every
deploy — they are not stored rows, so there is nothing for the ladder to copy.
A development site inheriting production’s variables still uses its own
database.
ghayma env import
Bulk-import environment variables from a dotenv file. Supports standard
dotenv syntax: KEY=VALUE, double- and single-quoted values, # comments,
blank lines, and optional leading export .
# Import into runtime vars
ghayma env import .env.production
# Mark every imported var as build-time
ghayma env import .env.production --build-time
# Preview without applying
ghayma env import .env.production --dry-run
# Don't overwrite keys that are already set
ghayma env import .env.production --skip-existing
# Override secret-pattern rejection for --build-time keys
ghayma env import .env.production --build-time --force
# Import into a specific site of a workspace
ghayma env import .env.production --site adminOutput (example with a mix of new/overwritten/skipped keys):
📄 .env.production → my-project (5 entries parsed)
+ Added (3):
DATABASE_URL
SMTP_URL
STORAGE_BUCKET
~ Overwritten (1):
API_KEY
· Skipped — already set (1):
FEATURE_FLAG
✅ Import applied. Redeploy with: ghayma deploy--dry-run prints the same summary but ends with:
🔍 Dry run — no changes made.ghayma env pull
Write the site’s effective environment to a local dotenv file: the variables you set plus every value the platform derives from the site’s connections — database URLs, storage credentials, the auth app’s variables, cron secrets and the managed platform key. It is exactly what the deployed app receives, so your local app runs against the same services.
# Write .env.local next to the app
ghayma env pull
# A specific site, and a file of your own
ghayma env pull --site admin --out .env.development.localThe file is .env.local next to the app — its directory in a
workspace — or the path you give with
--out. Keys are written in alphabetical order, so two pulls of the same site
diff cleanly. A connection still waiting for its own
credential is left out: the file
has no variables for it until the platform gives it one.
Output:
✅ Wrote 7 variables for 'main' to .env.local
DATABASE_URL (derived)
GHAYMA_API_KEY (derived)
LOG_LEVEL (own)
...
3 own · 0 inherited · 4 derived — derived values (database URLs, bucket keys, the managed key) are this site's own; inherited ones come from the default site.
Contains secrets — keep it out of git. Values are never printed here; open the file.Each name carries where its value came from — own, inherited from the
default site, or derived by the platform from this site’s connections. On a
site that inherits that is the answer to “which of these
is production’s?”; the file itself is unchanged, and has always held the
effective environment.
The values are secrets, so the command protects the file:
- a file git tracks is refused outright — the secrets would land in your next
commit. Untrack it, add it to
.gitignore, then re-run; - inside a repository, a file git does not ignore is refused too unless you
pass
--force, sincegit add .would stage it; - the file is written readable by you only;
- no value is ever printed — the command lists the variable names it wrote and nothing else.
Pulling effective variables needs the project admin role (owner or admin), because every value is a credential of a connected service; below that the command says so and asks you to talk to the project owner. Every pull is recorded in the project’s audit log — who, which site, from where, and how many variables, never the values or their names.
Options:
| Flag | Description |
|---|---|
--site | Site to pull, by name or slug |
--out | File to write (default: .env.local next to the app) |
--force | Write even if the file is not git-ignored (a tracked file is always refused) |
env pull needs the platform update that serves a site’s effective
variables, which arrives on the same release cycle as the command. Against a
platform without it the CLI writes nothing and says so: This platform does not
serve effective variables yet — the update that adds them is not deployed.
Until then: ghayma env list.
Automatically Injected Variables
A database, a storage bucket and an auth app belong to a project, and the
platform puts their connection variables into the containers of the apps
connected to them — you never set them yourself.
ghayma env pull writes them into a local file so an app
runs locally against the same services.
| Service | Variable | Example |
|---|---|---|
| PostgreSQL | DATABASE_URL | postgresql://c_14bb37f4:pass@host:5432/db_mydb |
| MongoDB | MONGODB_URI (alias MONGODB_URL) | mongodb://c_9a2f81c0:pass@host:27017/db_mydb?authSource=admin |
| Valkey | REDIS_URL (alias VALKEY_URL) | redis://c_3d7e5a19:pass@host:6379/0 |
| Storage | STORAGE_ENDPOINT | https://s3.ghayma.tech |
| Storage | STORAGE_ACCESS_KEY | GK... |
| Storage | STORAGE_SECRET_KEY | ... |
| Storage | STORAGE_BUCKET | st-myassets-a1b2c3d4 |
| Storage | STORAGE_REGION | <region> |
| Auth | GHAYMA_AUTH_APP_ID | my-auth-app |
| Auth | GHAYMA_AUTH_URL | http://auth-service.paas-system.svc.cluster.local:9000/v1/my-auth-app |
| Auth | GHAYMA_AUTH_PUBLIC_URL | https://auth.ghayma.tech/v1/my-auth-app |
| Auth | GHAYMA_AUTH_SERVER_KEY | ghs_... |
| Platform API | GHAYMA_API_KEY | gsk_... |
Those credentials are the app’s own, not the service’s. c_14bb37f4 is a
database role — a MongoDB user on a replica set — that the platform creates for
that one connection, and STORAGE_ACCESS_KEY holds an S3 key created for that
one connection, able to read and write that bucket and nothing else. Each
connected app gets its own, and disconnecting an app deletes only that app’s.
A connection waiting for its own credential is absent from env pull: the
platform gives it one as soon as the service runs (it retries every ten
minutes), and a deploy carries no value for it until then. See
Valkey, Each app gets its own database
credential and
Each app gets its own storage
key.
A read-only connection changes none of these names. A database connected at
read-only, or a bucket at read, injects exactly the same variables — the
credential inside them simply cannot write. See Read-only
connections.
Which sites get a database’s variables
A site gets a database’s variables exactly when it is connected to that database — the same connection that opens the network path, so no site is handed credentials it cannot use, or a network path without credentials.
- A new database is connected only to the sites you choose when you create it
(
--site) — none if you choose none, even in a project with a single site. A Valkey connects them once it is running (see Valkey). ghayma deployoffers a database no site uses to the site it deploys.- Connect more apps any time with
ghayma db sitesor from the database’s page in the dashboard.
ghayma db sites my-postgres # which apps are connected today
ghayma db sites my-postgres --add admin # connect the 'admin' site as wellA site connected to no database gets no database variables at all. See Connections and Which sites can reach a database.
One set of variables per database
The flat DATABASE_URL (PostgreSQL), MONGODB_URI (MongoDB) and REDIS_URL
(Valkey) point at the oldest database of that engine the site is connected
to, so adding a second database never repoints a running app. MongoDB also gets
MONGODB_URL, and Valkey VALKEY_URL: aliases carrying the same value for apps
written against that name.
Every connected database also gets names of its own — DATABASE_URL_<NAME>;
MONGODB_URI_<NAME> and MONGODB_URL_<NAME>; or REDIS_URL_<NAME> and
VALKEY_URL_<NAME>. <NAME> is the database name
upper-cased, with every character that is not a letter or a digit replaced by
_. A site connected to main-db (created first) and analytics therefore
reads:
DATABASE_URL → main-db
DATABASE_URL_MAIN_DB → main-db
DATABASE_URL_ANALYTICS → analyticsDerived, not stored
These variables are computed from the database itself on every deploy. They are not stored env vars, so:
- they don’t appear in
ghayma env list, and there is nothing to edit or delete; - a variable you set under one of these names is replaced by the platform’s
value whenever the site is connected to a database of that engine — in the
build as well as in the running app, a build-time value included (the
override is logged, never silent). Give your own value another name —
PRIMARY_DB_URL,ANALYTICS_URI. A Valkey’s names are replaced in the running app only: a build keeps a build-timeREDIS_URLof your own (see Valkey); - on a site connected to no database, nothing is derived and a
DATABASE_URLof your own — an external database, say — is left untouched.
They follow the database
Connecting or disconnecting an app, creating a database and deleting one all reach the project’s running apps immediately: the pods restart with the new values, no redeploy needed. Every later deploy derives the same variables again, and a rollback gets exactly what a deploy gets.
Rotating one app’s credential with ghayma connections rotate reaches that app the same
way, and no other. A database’s own login and a bucket’s own key are never in
these variables, never shown and never rotated by people — only an operator
rotates them, and the connected apps keep running on their own credentials,
unchanged.
Internal vs external URLs: the derived URL uses the database’s internal in-cluster hostname, which is the one a deployed app must use. The URL a named principal is handed is the other one — for connecting from outside Ghayma, like your laptop. URLs you do set by hand for other services need their internal form too. A Valkey has no external URL yet: reach it from your laptop through the tunnel. See Valkey and the Networking Guide.
Your auth app’s variables
A site connected to an auth app is handed that app’s identifier, its two endpoints and its server key, so an app deployed on Ghayma reaches its own auth app with no configuration at all:
| Variable | What it is |
|---|---|
GHAYMA_AUTH_APP_ID | The app’s slug — also the audience a JWT is verified against |
GHAYMA_AUTH_URL | The in-cluster endpoint, already ending in /v1/<app-slug>. Server-to-server calls only |
GHAYMA_AUTH_PUBLIC_URL | The browser-reachable endpoint. OAuth redirects and login links |
GHAYMA_AUTH_SERVER_KEY | The app’s server key — a secret, so server-side code only |
The flat names point at the oldest auth app the site is connected to, so
connecting a second one never repoints a running app. Every connected app also
gets names of its own —
GHAYMA_AUTH_APP_ID_<APP>, GHAYMA_AUTH_URL_<APP>,
GHAYMA_AUTH_PUBLIC_URL_<APP> and GHAYMA_AUTH_SERVER_KEY_<APP>.
<APP> is the app slug upper-cased, with every character that is not a letter
or a digit replaced by _. A site connected to main-auth (created first) and
staff-portal therefore reads:
GHAYMA_AUTH_APP_ID → main-auth
GHAYMA_AUTH_APP_ID_MAIN_AUTH → main-auth
GHAYMA_AUTH_APP_ID_STAFF_PORTAL → staff-portalThese are derived exactly like the database variables: computed on every deploy,
never stored, and absent from ghayma env list. There is nothing to set by hand
any more — and a value you set yourself under one of these names is replaced by
the platform’s whenever the site is connected to an auth app (the override is
logged, never silent), so give your own value another name. A site connected to
no auth app derives none of them, and a GHAYMA_AUTH_URL of your own —
another project’s app, say — is left untouched.
The app id in a frontend build
A frontend needs the auth app’s slug in the browser bundle, and bundlers inline those values while the image is built, long before the container’s environment exists. The app id is not a secret, so it is handed to the build as well:
GHAYMA_AUTH_APP_ID— the app id, available as a build argument and at runtime.NEXT_PUBLIC_GHAYMA_AUTH_APP_ID— the same value under the namenext buildinlines, derived on sites whose framework is Next.js.
On a Next.js site there is nothing to configure:
const auth = new GhaymaAuth({ appSlug: process.env.NEXT_PUBLIC_GHAYMA_AUTH_APP_ID });Every other bundler has its own public prefix, and the platform does not guess
one — map GHAYMA_AUTH_APP_ID yourself in the build config, or set the public
variable by hand with ghayma env set --build-time. One line
for Vite:
export default defineConfig({
define: {
"import.meta.env.VITE_GHAYMA_AUTH_APP_ID": JSON.stringify(process.env.GHAYMA_AUTH_APP_ID),
},
});Server-side code needs none of this: it reads GHAYMA_AUTH_APP_ID from the
running container’s environment.
The managed platform key
A site connected to a storage bucket, a database, or an auth app at level
admin also gets GHAYMA_API_KEY — a project API key the
platform mints for that site — so the Server SDK needs no
argument at all:
import { Ghayma } from "@ghayma/sdk";
const ghayma = new Ghayma(); // reads GHAYMA_API_KEYThe key is named site:<site> and carries exactly the capabilities the site’s
connections imply: storage for a bucket, databases
for a database, and auth for an auth app connected at level admin — narrowed
to those apps, so the project’s other auth apps are never swept in. It is listed
under Project → Settings → API keys as managed, and it follows the
connections rather than your hand — it cannot be rolled or revoked there, it is
re-minted when they change, and disconnecting the site’s last service revokes
the key. A change reaches the running pods without a redeploy, like the database
and storage variables.
Administering an auth app’s end users therefore needs that app connected at
admin; at client — the level a new connection starts at — the key cannot
touch its users. Create an Application key by hand for any server that runs
outside Ghayma.