Skip to Content
CLI ReferenceEnvironment Variables

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-123

Output:

✅ Environment variables updated: DATABASE_URL=postgres://... API_KEY=sk-123

When --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 deploy

Changes 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 admin

Output:

🔧 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_KEY

Each key is reported on its own line:

✅ API_KEY removed 🔄 Redeploy to apply: ghayma deploy

A 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 again

Deleting 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 admin

Output (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.local

The 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, since git 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:

FlagDescription
--siteSite to pull, by name or slug
--outFile to write (default: .env.local next to the app)
--forceWrite 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.

ServiceVariableExample
PostgreSQLDATABASE_URLpostgresql://c_14bb37f4:pass@host:5432/db_mydb
MongoDBMONGODB_URI (alias MONGODB_URL)mongodb://c_9a2f81c0:pass@host:27017/db_mydb?authSource=admin
ValkeyREDIS_URL (alias VALKEY_URL)redis://c_3d7e5a19:pass@host:6379/0
StorageSTORAGE_ENDPOINThttps://s3.ghayma.tech
StorageSTORAGE_ACCESS_KEYGK...
StorageSTORAGE_SECRET_KEY...
StorageSTORAGE_BUCKETst-myassets-a1b2c3d4
StorageSTORAGE_REGION<region>
AuthGHAYMA_AUTH_APP_IDmy-auth-app
AuthGHAYMA_AUTH_URLhttp://auth-service.paas-system.svc.cluster.local:9000/v1/my-auth-app
AuthGHAYMA_AUTH_PUBLIC_URLhttps://auth.ghayma.tech/v1/my-auth-app
AuthGHAYMA_AUTH_SERVER_KEYghs_...
Platform APIGHAYMA_API_KEYgsk_...

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 deploy offers a database no site uses to the site it deploys.
  • Connect more apps any time with ghayma db sites or 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 well

A 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 → analytics

Derived, 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-time REDIS_URL of your own (see Valkey);
  • on a site connected to no database, nothing is derived and a DATABASE_URL of 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:

VariableWhat it is
GHAYMA_AUTH_APP_IDThe app’s slug — also the audience a JWT is verified against
GHAYMA_AUTH_URLThe in-cluster endpoint, already ending in /v1/<app-slug>. Server-to-server calls only
GHAYMA_AUTH_PUBLIC_URLThe browser-reachable endpoint. OAuth redirects and login links
GHAYMA_AUTH_SERVER_KEYThe 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-portal

These 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 name next build inlines, 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:

vite.config.ts
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_KEY

The 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.