Sites
A site is an independently deployable app within a project. Each site has its own deployments, domains and environment variables, and uses the project’s databases, buckets and auth apps through the connections you give it.
Example use cases:
frontend+admin+apiin one project- A monorepo where each package is its own site
- A/B deployments or staging environments
A site also carries an environment kind — production, staging or
development — which is what makes a deploy a production one, what lets a site
inherit the default site’s variables, and what a
promotion moves
between. See Environments.
Site URLs
Every site is served at its own .ghayma.app URL, and the rule that picks it has
exactly two cases:
| Site slug | Hosted at |
|---|---|
main | <project-slug>.ghayma.app |
| anything else | <project-slug>-<site-slug>.ghayma.app |
So project acme with sites main, admin and api serves
acme.ghayma.app, acme-admin.ghayma.app and acme-api.ghayma.app.
You never have to derive this yourself — every site the API returns carries a
computed default_domain field. See Sites in the API
reference.
Custom domains are attached per-site on top of this URL and are never affected by renames — see Domains.
How sites work
A brand-new project has no sites at all. The main site is created lazily,
on your first app action — the first ghayma deploy, or attaching a custom
domain. If you only have one app, you never need to think about sites:
everything lands on main.
main is not special beyond its URL. It can be renamed and it can be deleted,
and a project holding several custom sites with no main at all is a perfectly
normal steady state.
When you run ghayma init and choose to deploy an app from the directory,
you’re asked for a site name. Press Enter to use main, or type a name like
frontend. Answer No site instead and the project starts with none — see
Projects without a site.
The .ghayma.json config file stores the active site:
{
"project_id": "uuid",
"name": "my-project",
"slug": "my-project",
"framework": "nextjs",
"site_id": "uuid",
"site_name": "frontend",
"site_slug": "frontend"
}Every deploy, env, and domain command run in that directory targets the
site stored in this file.
That is the single-app layout: one directory, one site. A workspace repo can
instead hold one manifest at its root listing every site and the directory
it builds from — then you pick the site per command with --site <slug>, or
just run the command inside the app’s own directory. See Workspaces
(monorepos).
When no site is named
If a command or API call names a project but no site, the platform walks a fixed ladder to decide which site you meant:
- The project’s default site, if one is set
- Otherwise the site named
main - Otherwise the only site, if the project has exactly one
- Otherwise it refuses and asks you to name a site or set a default
Rung 4 is a real outcome, not an edge case: once you have two custom sites and
no main, nothing is implicit any more. Set a default from the Dashboard, or
pass the site explicitly (ghayma site use <slug>, or --site where the
command accepts it).
The one exception is a project with no sites at all — there, a first deploy
or a custom-domain attach creates main rather than failing. An ambiguous
project never gets a site invented for it.
A new database, bucket or auth app does not use this ladder. It is
connected only to the sites you choose when you create it — none if you choose
none, even in a project with one site — and creating a site connects nothing.
ghayma deploy offers the services no site uses to the site it deploys. See
How a service gets
connected.
ghayma site list
List all sites in the current project. The active site (from
.ghayma.json) is marked with ▶.
ghayma site listOutput:
📌 Sites:
▶ frontend (slug: frontend, env: development, status: active, id: abc123...) [inherits env]
admin (slug: admin, env: production, status: active, id: def456...)
main (slug: main, env: production, status: active, id: ghi789...)
To switch active site: ghayma site use <slug>env: is the site’s environment kind, and
[inherits env] marks a site that resolves its variables through the default
site’s — see Environment variables are
inherited.
ghayma site create
Create a new site in the current project.
ghayma site create frontendOr run without arguments for an interactive prompt:
ghayma site createOutput:
✅ Site 'frontend' created (slug: frontend, id: abc123...)
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 frontend
ghayma deploySite names are slugified automatically. "My API" becomes slug my-api.
Options:
| Flag | Description |
|---|---|
--env | Environment kind of the new site: development (default), staging or production. dev, stage and prod are accepted too |
ghayma site create dev --env development
ghayma site create admin --env productionA new non-production site also starts with environment-variable
inheritance on: it resolves the project’s default site’s variables as its
base and overrides them key by key, so standing one up is not a hand-copy of
production’s list. Credentials are never inherited — a development site gets its
own DATABASE_URL through its own connection. See
Environments.
An unknown kind is rejected before the request is sent:
❌ unknown environment "developement" — use production, staging or development.
In a linked workspace, site create offers to add the new site to the root
manifest and asks which directory holds it — skip that and the site is created
but left out of the manifest until you add it yourself or re-run ghayma link.
See Workspaces (monorepos).
Two names are rejected up front: one that slugifies to a site you already have
(409), and one so long that the site’s Kubernetes namespace would exceed 63
characters (422).
A new site starts with no build settings — it auto-detects until you deploy
a .ghayma.json or fill them in on the site’s Settings page. Nothing is copied
from a sibling site.
The CLI creates on the plan’s default tier. site create has no sizing
flag yet — resize afterwards with ghayma site scale.
The console’s New Site form and the API take
the tier and replica count at create instead, which is one points admission
rather than two.
ghayma site use
Switch the active site in .ghayma.json. All subsequent deploy,
env, and domain commands will target this site.
ghayma site use adminOutput:
✅ Active site switched to 'admin' (slug: admin)
Run 'ghayma deploy' to deploy to this siteYou can switch by slug or by name. Switching preserves the rest of the file,
including dockerfile_path and crons.
Not available at a workspace manifest root. A manifest has no single
active site to switch — every command either takes --site or asks. Run
site use inside the app’s own directory, or name the site per command:
ghayma deploy --site admin --prod. See Workspaces
(monorepos).
ghayma site environment
Change which environment a site is — production,
staging or development.
ghayma site environment staging staging
ghayma site environment preview developmentOutput:
✅ 'staging' is now a staging site (was development)
It applies from the next deploy of this site.The site is positional and required: re-kinding changes what that site’s
deploys count as and who may touch them, so it is never inferred from the
directory you happen to be in. A site that is already that kind is left alone —
ℹ️ 'staging' is already a staging site — no change.
A deployment counts as production exactly when the site it targets is a production site. The kind also decides how the CLI and the dashboard warn about sharing a database across environments.
The project’s default site is always production and cannot be
re-kinded — it is the site served at the project’s bare URL:
the default site is always the production environment and cannot be changed; make another site the default first. Create another site for the environment
instead, or make a different site the default first.
ghayma site inherit-env
Turn environment-variable inheritance on or off for a site.
ghayma site inherit-env dev on
ghayma site inherit-env dev offOutput:
✅ 'dev' now inherits the default site's variables, overridden by its own
See what it resolves to: ghayma env list --site devWith it on, the site resolves the project’s default site’s variables as its base, overridden key by key by its own — set a variable of the same name here to override an inherited one. New non-production sites start with it on.
With it off, the site resolves its own rows only. Nothing is copied down first:
✅ '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.Credentials are never inherited whatever this is set to: database URLs, bucket keys, cron secrets, the auth variables and the managed platform key are derived per site from that site’s own connections. A development site inheriting production’s variables still uses its own database. See Environments.
The default site cannot inherit — it is the base the others read from:
the default site is the base of the environment ladder and cannot inherit; the sites that inherit FROM it are the secondary ones.
ghayma site scale
Change an app’s compute tier and/or replica count — both priced in
points. Both flags are optional: an omitted --tier keeps the current tier, an
omitted --replicas keeps the current count. Scales the project’s active or only
site unless you pass --site.
# Bigger tier, same replicas
ghayma site scale --tier c
# More replicas, same tier
ghayma site scale --replicas 3
# Scale a specific site by tier and replica count
ghayma site scale --site api --tier b --replicas 2Options:
| Flag | Description |
|---|---|
--tier | New compute tier (e.g. a, b, c, d). Keeps the current tier when omitted. |
--replicas | New replica count (must be >= 1). Keeps the current count when omitted. |
--site | Site name or slug to scale. Defaults to the project’s active or only site. |
Output:
📌 my-app (slug: main)
Current: tier b · 2 replica(s)
c × 3 = 36 pts · 14 remaining after
✅ 'my-app' scaled to tier c · 3 replica(s)
The app is moving to the new size now, with no downtime.The current size is shown first, then the new points footprint (tier × replicas) is previewed before the change is sent. The · N remaining after tail
accounts for the difference a resize spends, and appears only when your
project has a points budget.
The running app changes right away. A new tier rolls the app onto the new size: a pod with the new size starts and takes traffic before an old one stops, so the app stays up. A replica change alone adds or removes pods without restarting the others. The last line says whether the running app is moving now; when it is not, it says when the new size takes effect instead:
The site has not been deployed yet; its first deploy uses the new size.A smaller tier must fit the app’s memory. Moving to a tier with less memory is refused when the app used more than 90 % of that tier’s memory at any point in the last 7 days, because the smaller app would run out of memory and be restarted. Nothing changes, and the message names the smallest tier that fits:
❌ Failed to scale: the app used up to 615 MB of memory in the last 7 days, which does not fit tier a with room to spare; the smallest tier it fits is bCPU is not checked: a smaller CPU limit slows the app down, it does not stop it. An app with no memory use recorded yet (never deployed, or not running) can move to any tier.
Tier prices and points come from the live catalog — the figures above are only an example.
Notes:
- Replicas must be
>= 1. Pause / scale-to-zero isn’t supported yet, so--replicas 0is rejected before any request:❌ replicas must be >= 1 (pause / scale-to-zero is not yet supported). - An app that has never been sized has no compute tier yet — set one by passing
--tier:❌ this app has no compute tier yet — specify --tier <tier> to set one. - Passing neither flag does nothing:
Nothing to change. Pass --tier and/or --replicas. - CLI versions before v0.11.10 print only the
✅line. The running app still changes right away; only the message is missing. - Check what any app reserves with
ghayma points; for the pricing model see Points & Plans.
Renaming a site
The CLI has no site rename command yet. Rename from the
Dashboard or with Update
Site in the API.
Renaming a site is two very different operations depending on whether the new name produces the same slug:
Cosmetic rename. "api" → "API" both slugify to api. Only the display
name changes, nothing moves, and it takes effect immediately.
URL-moving rename. "api" → "public-api" derives a new slug, and the slug
is the site’s identity: its URL, its Kubernetes resources, its container image
repository and its network policies all carry it. So this one moves the site’s
URL, has to be acknowledged explicitly, and runs as a background migration —
your app keeps serving on the old URL until the cutover, then serves on the new
one.
Renaming to or from main works the same way. A site renamed to main takes
over the project’s bare <project>.ghayma.app URL; a main site renamed to
anything else gives it up.
While a rename is running, deploys, rollbacks and deletes on that site are rejected — and a rename is rejected while any of those are in flight.
The old URL keeps working for 30 days
After a URL-moving rename, the site’s old .ghayma.app URL is not deleted.
It becomes a temporary redirect to the new URL for 30 days, so links,
bookmarks, webhook registrations and CI scripts you already shipped keep landing
on your app while you migrate them. After 30 days a daily sweep removes it and
the old URL stops resolving.
- The redirect is temporary on purpose (302 for browsers, 307 for other
methods, so a
POSTto an old webhook URL still arrives as aPOST). A permanent redirect would be cached by browsers long after the window closed. - The path is preserved:
old.ghayma.app/webhooks/stripe→new.ghayma.app/webhooks/stripe. - Each site keeps at most one grace redirect, so repeated renames never chain. Renaming back — or re-creating a site under the old name — reclaims the old URL immediately.
- Custom domains are never touched by a rename. They stay attached to the site and keep serving throughout.
The same 30-day grace applies when you rename the project: every one of its sites keeps its old URL as a redirect. See Rename Project.
Deleting a site
The CLI has no site delete command yet. Delete from the
Dashboard or with Delete
Site in the API.
Any site can be deleted, including main and including the default site.
Deleting the last site leaves the project site-less — exactly how it started —
and the next deploy re-creates main.
Deleting a site removes, in one step:
- the running app, its deployment history and its environment variables
- every domain attached to it, including its default
.ghayma.appURL - its cron jobs and their run history
- its connections to the project’s databases, buckets and auth apps
Purchased-domain records survive — you paid for those — they are just unlinked from the deleted site and can be re-attached elsewhere.
Deleting the main or the default site needs explicit confirmation —
these are the two deletes that used to be refused outright. Deleting main
takes down the project’s bare <project>.ghayma.app URL; deleting the default
site leaves the project with no default until you set another. The Dashboard
asks and lists the consequences; the API requires {"confirm": true} in the
request body. Any other site deletes without ceremony.
Deletes are refused while a deployment is building on that site, or while the site or its project is being renamed. Wait for the operation to finish and retry.
Because points are charged for what you actually run, dropping a site releases its app and cron-job points immediately — there is no separate refund step.
Multi-site workflow
Monorepo example
Recommended — one manifest at the workspace root:
my-project/
├── .ghayma.json ← manifest: frontend → apps/frontend, admin → apps/admin
├── apps/
│ ├── frontend/
│ └── admin/
└── packages/
└── shared/cd my-project
ghayma link my-project # choose "Whole project" — maps every site to a directory
ghayma deploy --site frontend --prod
ghayma deploy --site admin --prodOne link covers every app in the workspace, and deploy from the root asks
which site you meant when you don’t pass --site. See Workspaces
(monorepos).
Alternative — a config file inside each app:
my-project/
├── apps/
│ ├── frontend/ ← .ghayma.json (site: frontend)
│ └── admin/ ← .ghayma.json (site: admin)
└── packages/
└── shared/Two equivalent setup paths:
Option A — init from inside each app directory:
cd apps/frontend
ghayma init # name: my-project, site: frontend
ghayma deploy --prod
cd ../admin
ghayma init # same project_id, site: admin
ghayma deploy --prodOption B — init from the monorepo root:
If you run ghayma init from the monorepo root (where turbo.json, or a
pnpm-workspace.yaml with a packages: list, lives), the CLI detects the
workspace and asks for an app subdirectory first. It then writes .ghayma.json inside that
subdir with root_directory set, so deploys pick up the whole workspace
but build the right target. See Monorepo initialisation.
Per-app files keep working after you link the workspace, and link copies
their build settings into the manifest — so you can move to a manifest without
touching a live build.
Each site gets its own Kubernetes deployment, its own URL, and its own env vars.
Picking a site per command
With a manifest at the workspace root, there is nothing to switch — say which site you mean, or let the directory say it for you:
# Name the site
ghayma deploy --site admin --prod
ghayma env list --site admin
# Or run from the app's own directory — the site is implied
cd apps/admin && ghayma deploy --prodWith a config file inside each app, switch the active site instead:
# Check which site is active
cat .ghayma.json | grep site_name
# Switch to a different site
ghayma site use admin
# Deploy to admin site
ghayma deploy --prod
# Switch back
ghayma site use frontendEach site has its own
- Environment kind —
production,stagingordevelopment, which is what makes a deploy a production one. The default site is always production — see Environments - Deployments — independent build + deploy history
- URL —
{project-slug}-{site-slug}.ghayma.app(exceptmain, which uses{project-slug}.ghayma.app) — see Site URLs - Domains — custom domains are attached per-site
- Environment variables —
ghayma env setapplies to one site: the one you’re in, or the one you name with--site - Build settings — framework, install/build/start commands, output directory, port, Dockerfile path, and whether to always generate the Dockerfile. Edit them on the site’s Settings page, or declare them per site in
.ghayma.json— see Where build settings live - Cron jobs — attached to a site and removed with it
Sites share
- Databases — owned by the project, not by a site; each app reaches one through a connection, set with
ghayma db sitesor in the dashboard, and a connected site also receives that database’s connection variables - Storage buckets — owned by the project; an app receives a bucket’s credentials when it is connected to it
- Auth apps — owned by the project; an app receives an auth app’s variables when it is connected to it
- Teams & billing — project-level
- Bandwidth & usage — metered per project: every site’s bandwidth and compute usage is summed into the project’s usage, and draws down the one plan bandwidth allowance — see Points & Plans
- The custom-Dockerfile permission — the Use custom Dockerfile switch in project Settings → Advanced applies to every site — see Custom Dockerfile
Dashboard
The dashboard at dash.ghayma.cloud is where site renames and deletes live today:
- Open your project
- Click Sites in the left sidebar
- Create, view, rename and delete sites — including
main - Set which site is the project’s default (the one implicit calls resolve to)
- Click Env Vars or Domains next to any site to manage them directly
- Open a site and click Settings for its build settings — framework, install/build/start commands, output directory, port, Dockerfile path, and Always generate the Dockerfile for this site. The project’s Settings tab points you here; editing needs the admin or owner role, everyone else sees them read-only
A rename that changes the site’s URL shows you the old and new URL and asks you to agree before it starts, then reports progress while the migration runs. Deleting the main or default site asks for confirmation and lists exactly what stops working.