Connections
A connection records that one site may use one service — a database, a storage bucket or an auth app — at one level. It drives the variables the site’s containers receive, its managed platform key and, for a database, the network policy. See the Connections guide for the concept.
Nothing connects on its own. A connection exists because someone chose it: at
create (connect_site_ids on databases,
buckets and auth
apps), when ghayma deploy asks, or through
the routes below.
All endpoints require Authorization: Bearer TOKEN. :id is a project id or
slug; :siteId is a site uuid of that project.
These routes take an account token (gh_…) or a dashboard session. A
project API key (gsk_…) answers 403: a site’s managed
key is minted from its own connections, so a key able to change them could
widen what it may do.
Kinds and Levels
Every kind takes two levels, listed here weakest first.
kind | level | Grants |
|---|---|---|
database | read-only | The connection variables and the network path, with a credential that may only read |
database | connect | The connection variables and the network path |
bucket | read | The bucket’s S3 credentials, with a key that may only GET and LIST |
bucket | read-write | The bucket’s S3 credentials |
auth_app | client | The app id, both endpoints and the server key |
auth_app | admin | The above, plus auth on the site’s managed GHAYMA_API_KEY, narrowed to that app |
Omit level on a write and the kind’s default is used — connect for a
database, read-write for a bucket, client for an auth_app. A level
the kind does not accept is a 400. Reads need the project read role; every
mutation needs write.
Connecting a site to a service also creates a credential for that connection
alone: a database role, or a MongoDB or Valkey user, named c_ followed by
eight characters, or an S3 access key scoped to that one bucket. The site’s
DATABASE_URL, MONGODB_URI, REDIS_URL and
STORAGE_ACCESS_KEY/STORAGE_SECRET_KEY carry it instead of the service’s own
credential, and disconnecting deletes that credential and
nothing else. See Each app gets its own database
credential and
Each app gets its own storage
key.
A connection that cannot get a credential of its own is not made. Two refusals
say why, both on POST and PUT:
| Status | code | When |
|---|---|---|
409 | not_running | The database or bucket is stopped, or still being provisioned: “the service is not ready: start it (or let it finish provisioning), then try again”. Connect it once it runs |
409 | credential_unsupported | The service cannot give an app a credential of its own: “this service cannot give an app a credential of its own, so the connection was not made” |
A Valkey that is not running answers 409 with code: database_not_running
instead (see Valkey).
The level does not change env_names or the network path; it changes only what
the credential may do. See Read-only
connections.
Connection Object
Every endpoint returns this shape:
{
"site_id": "b0a1…",
"site_slug": "admin",
"kind": "database",
"resource_id": "9f3c…",
"resource_name": "conn-db",
"level": "connect",
"env_names": ["DATABASE_URL", "DATABASE_URL_CONN_DB"],
"created_at": "2026-09-08T10:00:00Z"
}resource_name is the database’s or bucket’s name, or the auth app’s
app_id.
env_names
env_names holds the exact variable names the platform injects into that site
for that connection — names only, never values. Read the values with
ghayma env pull.
| Connection | env_names |
|---|---|
PostgreSQL database conn-db, the site’s only database | DATABASE_URL, DATABASE_URL_CONN_DB |
Valkey database cache, the site’s only Valkey | REDIS_URL, VALKEY_URL, REDIS_URL_CACHE, VALKEY_URL_CACHE |
Bucket conn-files | the five STORAGE_* names, and the same five as STORAGE_CONN_FILES_* |
Auth app azeghj | GHAYMA_AUTH_APP_ID, GHAYMA_AUTH_URL, GHAYMA_AUTH_PUBLIC_URL, GHAYMA_AUTH_SERVER_KEY, and the same four names again suffixed _AZEGHJ |
The suffix is the service’s name upper-cased, with every character that is not
A–Z or 0–9 replaced by _. The first service of each kind the site
was connected to also holds the plain, unsuffixed names; every service holds the
suffixed ones. The full derivation is in Automatically injected
variables.
List Project Connections
GET /api/v1/projects/:id/connections| Query | Description |
|---|---|
site_id | Only this site’s connections |
kind | Only database, bucket or auth_app |
resource_id | Only connections to this resource — with kind, this is “which apps use this service” |
Response: 200
{
"connections": [
{
"site_id": "b0a1…",
"site_slug": "main",
"kind": "database",
"resource_id": "9f3c…",
"resource_name": "conn-db",
"level": "connect",
"env_names": ["DATABASE_URL", "DATABASE_URL_CONN_DB"],
"created_at": "2026-09-08T10:00:00Z"
}
]
}A project with no connections returns { "connections": [] }.
List a Site’s Connections
GET /api/v1/projects/:id/sites/:siteId/connectionsReturns what the site is connected to and what else it could be connected to, so a UI needs one call.
Response: 200
{
"connections": [
{
"site_id": "b0a1…",
"site_slug": "admin",
"kind": "auth_app",
"resource_id": "7d21…",
"resource_name": "main-auth",
"level": "client",
"env_names": ["GHAYMA_AUTH_APP_ID", "GHAYMA_AUTH_URL", "…"],
"created_at": "2026-09-08T10:00:00Z"
}
],
"available": [
{
"kind": "bucket",
"resource_id": "4e88…",
"resource_name": "conn-files",
"levels": ["read", "read-write"],
"env_names": [
"STORAGE_ENDPOINT", "STORAGE_ACCESS_KEY", "STORAGE_SECRET_KEY",
"STORAGE_BUCKET", "STORAGE_REGION",
"STORAGE_CONN_FILES_ENDPOINT", "STORAGE_CONN_FILES_ACCESS_KEY",
"STORAGE_CONN_FILES_SECRET_KEY", "STORAGE_CONN_FILES_BUCKET",
"STORAGE_CONN_FILES_REGION"
]
}
]
}available lists the project’s services this site does not hold, each with
the levels it accepts, weakest first — ["read-only", "connect"] for a database,
["read", "read-write"] for a bucket, ["client", "admin"] for an auth app. A deleted database and a disabled auth app never appear.
An available entry carries env_names too — there it is what
connecting the service would add.
List Unconnected Services
GET /api/v1/projects/:id/connections/unconnectedThe project’s databases, buckets and auth apps that no site is connected to
— what the console’s banner names and what ghayma deploy offers.
Response: 200
{
"unconnected": [
{ "kind": "database", "resource_id": "9f3c…", "resource_name": "shop-db", "default_level": "connect" }
]
}default_level is the level a connection made without one gets. A project whose
services are all in use returns { "unconnected": [] }.
Replace a Site’s Connections
PUT /api/v1/projects/:id/sites/:siteId/connectionsBody — the full desired set (an empty array disconnects everything):
{
"connections": [
{ "kind": "database", "resource_id": "9f3c…", "level": "connect" },
{ "kind": "auth_app", "resource_id": "7d21…", "level": "admin" }
]
}Response: 200 — the site’s connections after the write, in the
connection object shape. Only what actually moved is
re-derived, so saving an unchanged set restarts nothing.
Connect a Service
POST /api/v1/projects/:id/sites/:siteId/connectionsConnects one service, or changes the level of a connection that already exists.
Body:
{ "kind": "auth_app", "resource_id": "7d21…", "level": "admin" }Response: 201 — the stored connection object.
Disconnect a Service
DELETE /api/v1/projects/:id/sites/:siteId/connections/:kind/:resourceIdResponse: 200
{ "removed": true }removed is false when there was no such connection. Disconnecting twice is
not an error, so a retry after a dropped response is safe.
Rotate a Connection’s Credential
POST /api/v1/projects/:id/sites/:siteId/connections/:kind/:resourceId/rotateGives that one connection’s credential a new secret. Nothing else moves: not the
level, not the env_names, not another site’s credential, and never the
service’s own credential. kind is database or bucket — an auth_app has
no per-connection credential, and rotating one answers 409 with
code: nothing_to_rotate: “this kind of connection carries no credential of
its own”.
Response: 204 — no body. The new secret is never returned; it reaches the
app through the live patch this fires and through the next deploy’s derivation.
See Rotating Credentials.
A 409 means there is nothing to rotate right now: the connection has no
credential of its own yet, or the service is stopped. With no code, the
connection is still waiting for its own credential — “this connection has no
credential of its own yet, so there is nothing to rotate — the platform gives it
one as soon as the service can (it retries every ten minutes); retry later, or
reconnect it”. With code: not_running, the database or bucket is stopped or
still being provisioned: start it, then rotate. A Valkey that is not running
answers code: database_not_running instead (see
Valkey). Branch on code first. A 503 is
an engine that could not be reached: nothing changed, and the same call works
later.
Errors
| Code | When |
|---|---|
400 | Unknown kind, a level that kind does not accept, or a resource that belongs to another project |
403 | The caller’s project role is too low — or the caller is a project API key |
404 | No such project, :siteId is not a site of it, :siteId is not a uuid, or — on a rotate — the service is not connected to that site |
409 | Rotate, with no code: the connection has no credential of its own yet. The platform gives it one as soon as the service can (it retries every ten minutes); retry later, or reconnect it |
409 | With code: not_running (POST, PUT and rotate): the database or bucket is stopped or still being provisioned. Start it, then try again |
409 | With code: credential_unsupported (POST, PUT): the service cannot give an app a credential of its own, so the connection was not made |
409 | With code: nothing_to_rotate (rotate): an auth app connection, which carries no credential of its own |
409 | With code: database_not_running (POST, PUT and rotate): the database is a Valkey that is not running yet. Connect it, change its level or rotate it once it runs |
503 | POST, PUT and rotate: the service could not prepare or replace the site’s own credential — a database that was unreachable at that moment, for example. Retrying later works |
The per-service routes GET/PUT /databases/:id/sites
and GET/PUT /storage/:id/sites keep working. They read and write these
same connections, one service at a time. A PUT needs write on every
site whose access it changes.