Skip to Content
Framework GuidesEnvironments

Environments

Production, staging and development on Ghayma are not a separate feature bolted onto your project — they are sites. A site already has its own URL, its own environment variables, its own database and storage credentials, its own deployment history and its own compute tier. Naming which kind of environment a site is turns all of that into the isolation you want, and adds the three things sites did not have: inherited variables, promotion, and per-collaborator site access.

The three kinds

Every site carries an environment kind:

KindWhat it means
productionServes real traffic. Its deploys are the ones badged Production
stagingA pre-production copy — verify a release there, then promote it
developmentA working environment. New sites start here

The project’s default site is always production. It is the site served at the project’s bare <project>.ghayma.app URL and the fallback target of a project domain, so it cannot be re-kinded and it cannot inherit variables. Ask for it anyway and the platform says so:

❌ the default site is always the production environment and cannot be changed; make another site the default first 'main' is the site served at the project's bare URL. Create another site for this environment instead: ghayma site create <name> --env <kind>

The same invariant holds from the other side: making a development site the project’s default is refused with only a production site can be the project’s default site; change its environment to production first.

Nothing changed for projects that already existed. Every site created before this feature is production and inherits nothing — exactly the behaviour it had. Environments only start applying when you create a new non-production site, or re-kind an existing one.

Create a development environment

ghayma site create dev --env development
✅ Site 'dev' created (slug: dev, id: 4f3a91c8...) Environment: development · inherits the default site's variables (override any of them with 'ghayma env set') To deploy to this site, switch to it first: ghayma site use dev ghayma deploy

--env takes production, staging or development, and accepts the short forms prod, stage and dev. Omit it and a new site is a development site. A typo never reaches the server:

❌ unknown environment "developement" — use production, staging or development

What the new environment comes with

  • Its own URL<project>-dev.ghayma.app, alongside production’s. See Site URLs.
  • Its own environment variables — starting from production’s, through the inheritance ladder below.
  • Its own credentials. A database, bucket or auth app is connected to this site separately, and the platform mints this site’s own role, key and managed API key for it. Nothing is shared with production because it happens to carry the same variable name. See Connections.
  • Its own deployments — its own history, its own rollback window, its own build settings, its own cron jobs and its own container image repository.
  • Its own compute. That is also the honest cost: a development environment is a second app, and it reserves a full app tier priced exactly like production’s. There is no cheaper non-production tier yet. See Points & Plans.

ghayma site list shows the kind next to the slug, and marks a site that resolves its variables through the ladder:

📌 Sites: ▶ dev (slug: dev, env: development, status: active, id: 4f3a91c8...) [inherits env] main (slug: main, env: production, status: active, id: 9b2c17e0...) To switch active site: ghayma site use <slug>

Changing an existing site’s kind

ghayma site environment staging staging
✅ 'staging' is now a staging site (was development) It applies from the next deploy of this site.

The site is named explicitly and is never taken from the current directory: re-kinding changes what that site’s deploys count as, and who may touch them. See ghayma site environment.

Environment variables are inherited

A new non-production site would otherwise start with zero variables, and standing up a dev environment would mean hand-copying production’s list — a copy that goes stale in silence. Instead, a site with inheritance on resolves the default site’s variables as its base, overridden key by key by its own.

Inheritance is on by default for new non-production sites and off for every site that existed before the feature. The base is always the project’s default site.

ghayma env list --site dev
🔧 Environment variables for my-project: API_TIMEOUT=30 (inherited from main) FEATURE_FLAGS=beta LOG_LEVEL=debug NEXT_PUBLIC_API_URL=https://api.example.com [build-time] (inherited from main) 2 of 4 variable(s) are inherited from 'main'. Set the same key here to override one; credentials are never inherited.

Overriding one variable

Set the same key on this site and it wins — value, build-time marking and build-phase override together:

ghayma env set --site dev LOG_LEVEL=debug

The row stops being inherited and becomes the site’s own. Everything you did not override keeps following production, so a variable added to production later shows up here too.

Removing an override re-exposes the inherited value

Deleting your own row does not delete the variable — the base site’s value comes back, and the CLI says which happened:

ghayma env delete --site dev LOG_LEVEL
✅ LOG_LEVEL: the override is gone — the value inherited from 'main' applies again 🔄 Redeploy to apply: ghayma deploy

Deleting a variable you only inherit is refused

A variable that lives on the base site cannot be removed from the inheriting site. Succeeding silently would tell you the variable is gone while every deploy kept injecting it, so it is a 409 naming the site that actually holds 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>

Override it here, or edit it on main — where changing it changes it for production too, which is the point of the refusal.

Credentials are never inherited

DATABASE_URL never travels the ladder. Neither does MONGODB_URI, the STORAGE_* keys, the auth app’s variables, the CRON_SECRET_* signatures or the managed GHAYMA_API_KEY. A development site silently inheriting production’s database is exactly the accident environments exist to prevent.

This is structural rather than a filter somebody has to maintain: the ladder runs on stored variables only, and every credential above is derived at deploy time from the site’s own connections — it was never a stored row, so there is nothing for inheritance to copy. A dev site connected to dev-db gets DATABASE_URL pointing at dev-db, with a role minted for that one connection, whatever production’s DATABASE_URL says. See Derived, not stored.

ghayma env pull writes the effective environment and labels every row with where it came from, so the three kinds are never confused:

✅ Wrote 9 variables for 'dev' to .env.local API_TIMEOUT (inherited) DATABASE_URL (derived) LOG_LEVEL (own) ... 3 own · 2 inherited · 4 derived — derived values (database URLs, bucket keys, the managed key) are this site's own; inherited ones come from the default site.

Turning inheritance on or off

ghayma site inherit-env dev on ghayma site inherit-env dev off
✅ 'dev' now inherits the default site's variables, overridden by its own See what it resolves to: ghayma env list --site dev

Turning it off is always allowed, and nothing is copied down first — an implicit copy would be a silent duplicate of production’s configuration:

✅ 'dev' no longer inherits variables — it resolves its own rows only Anything it was inheriting stops being injected on its next deploy; nothing was copied down.

The default site cannot inherit; it is the base the others read from.

Don’t point a dev app at the production database

Two environments sharing one database is the failure mode environments exist to prevent: a migration, a seed script or a test that truncates a table runs in dev and destroys real data. So connecting across the production line — either direction — warns before it connects:

⚠️ orders-db is used by production site 'main'. A development site sharing a production database can corrupt real data; consider a separate database for 'dev' instead. Connect anyway? [y/N]

And connecting production to a database a non-production site already uses says the same thing from the other side:

⚠️ orders-db is used by development site 'dev'. A production database shared with a non-production site can be corrupted from there; consider a separate database for 'dev' instead.

The recommendation is a separate database for the non-production site. Create one, connect dev to it, and the flat-name rule exposes it there as that site’s own DATABASE_URL with no further configuration — see One set of variables per database.

It is a warning, not a block. There are legitimate reasons to share one — a read-only reporting app, a staging site deliberately pointed at a copy — so the connection is named, confirmed and made. --yes skips the question, not the warning: you still see what you are doing. The dashboard’s connect flow shows the same sentence and asks for the same explicit confirm.

Promote: ship the exact image you tested

ghayma promote deploys the image one site is already running onto another site. Nothing is rebuilt, because a rebuild is not the artifact you tested.

ghayma promote --from dev
ℹ️ Target: the project's default site 'main' (production) — pass --site <slug> to promote somewhere else. 🚀 Promoting dev → main (image sha256:0a1b2c3d4e5f) 📦 Promotion queued (deployment: 6f1c84a2-...) ⏳ Checking the image and rolling it out...
  • The target defaults to the project’s default site — the production one — and is always printed before anything is sent. --site <slug> promotes somewhere else; a site can never be promoted onto itself.
  • The source is --from, and it promotes that site’s current live deployment unless you name one with --deployment <id>.
  • The exact digest travels. The source deployment’s image is mounted into the target’s repository and re-tagged there — the same bytes, not a rebuild of the same commit.
  • The target’s own everything applies: its environment variables (including what it inherits), its connections and their credentials, its domains and its compute tier. A promote is an ordinary deploy of a known-good image.
  • The checks re-run. Platform, non-root user and image size are verified again against the target’s tier before anything is scheduled — production may be sized differently from dev, and production deserves the checks whatever dev decided. See The image contract.
  • Your tags can move later; the deployment doesn’t. Promotion resolves a digest, exactly like an image deploy.

It works whatever the source deployment was made from — uploaded source, a GitHub push or a pushed image — because by deploy time everything is an image with a digest. The result appears on the target’s Deployments tab with a promote badge, the source site and the digest that ran, and you can roll back to it or from it like any other deploy.

Promoting needs write on the target site. Read on the source is enough — looking up which image dev runs is not a change to dev.

From the dashboard, a deployment row on a non-production site carries a Promote to <site> action, which says exactly what it will do before you confirm it: Deploys this exact image to main now — its own env vars and connections apply. Checks re-run first.

Restricted collaborators

A collaborator can be restricted to named sites: their write or admin role applies on those sites, and they act as read everywhere else.

Set it from the dashboard: Project → Collaborators, then the site-access control on the person’s row — All sites, or Only these sites with the sites ticked. The editor says the one rule that matters out loud: Restricted collaborators keep read access across the project; they can only write on the sites listed here.

No sites listed means unrestricted — that is what every collaborator has until you say otherwise, and clearing the list is how you lift a restriction.

What a restricted person can and cannot do

Everything they can do:

  • See the whole project — every site, its pages, its deployments, its logs.
  • Deploy, roll back, edit variables, manage connections, crons, domains and build settings on the sites they are listed on.
  • Push images to the container repositories of those sites, and pull from any of the project’s.

Everything they cannot do, on a site they are not listed on:

  • Deploy to it — from the CLI, from the dashboard, or from a GitHub push. A push deploys as the person who linked the repository, so a dev-only collaborator who linked production’s repo sees the Git panel report Last push failed: deploy refused.
  • Roll it back, or promote into it.
  • Set, delete or pull its environment variables.
  • Connect, disconnect, re-level or rotate its databases, buckets and auth apps — or open a database tunnel with its credentials.
  • Add, change or remove its cron jobs, its custom domains, its build settings, its tier and replica count.
  • Re-kind it or change its inheritance.
  • ghayma docker push into its container repository — otherwise the deploy gate they cannot pass would be reachable from the side.

Every refusal is the same one:

insufficient role: your access is restricted to specific sites and this is not one of them

Owners and team members are never restricted. They hold project-wide access, and the dashboard refuses to restrict them: this member’s access cannot be restricted to sites: owners and team members hold project-wide access. A personal access token acts as its owner, so its owner’s restrictions apply to it unchanged.

Read stays project-wide. A restricted collaborator can still see production’s pages and its deployment history. The two reads that print secrets — ghayma env pull and a database tunnel — are restricted like a write, but restriction is a guard on changing things, not a way to hide them.

--prod is now informational

A deployment is a production one exactly when the site it targets is a production site. That is the whole rule, and --prod no longer decides anything:

ℹ️ --prod no longer changes anything: whether a deploy is production now follows the target site's environment (main is production).

The flag is still accepted — scripts and muscle memory carry it everywhere — and it is printed rather than ignored, so the answer arrives with the deploy rather than after it. The Production badge in the deployment history finally means what it says: before, the dashboard and git deploys hard-coded it true while the CLI defaulted it false, and the badge meant nothing.

Nothing else about a deploy changed. ghayma deploy still replaces what the target site serves, whichever kind of site that is.

From the dashboard

  • Sites are grouped by environment, production first, each with its kind badge; the kind picker is on the site’s page, showing the default site’s as a locked Production with the reason.
  • Env Vars greys the rows a site inherits, names the site they come from, and offers an override action on each one.
  • Connections shows the cross-environment warning before it connects a database, and asks for an explicit confirm.
  • Deployments on a non-production site carry the Promote to <site> action, and a promoted deployment shows a promote badge with the source site and digest.
  • Collaborators carries the site-access editor described above.

When something is refused

What you seeWhat it means
the default site is always the production environment and cannot be changed; make another site the default firstYou tried to re-kind the project’s default site. It carries the bare <project>.ghayma.app URL, so it is pinned to production — create another site for this environment, or make a different site the default first
the default site is the base of the environment ladder and cannot inherit; the sites that inherit FROM it are the secondary onesYou turned inheritance on for the default site. It is what the others inherit from; turn it on for one of them instead
<KEY>: this variable is inherited from <site>; override it or edit it there (409)The variable is not this site’s to delete. Set the same key here to override it, or edit it on the base site — which changes it for production too
insufficient role: your access is restricted to specific sites and this is not one of themYour write access is restricted and this site is not on your list. A project admin lifts or widens it on the Collaborators page
<site> has no live deployment to promote. Deploy it first, or name a deployment with "deployment_id". (409)The source site has never finished a deploy. Deploy it, or promote a specific one: ghayma promote --from <site> --deployment <id>
A site cannot be promoted onto itself. Name the site to promote FROM in "from_site".--from named the target. Promote from the site you tested on
That deployment's image (…) is no longer stored in the registry, so it cannot be promoted. (409)The image was pruned — it is older than the source site’s rollback window. Promote a newer deployment of that site, or deploy it again
only a production site can be the project's default site; change its environment to production firstYou tried to make a staging or development site the project’s default. Re-kind it to production first
this member's access cannot be restricted to sites: owners and team members hold project-wide accessOwners and team members are never restricted — only collaborators are