Skip to Content
API ReferenceDatabases

Databases

All endpoints require Authorization: Bearer TOKEN.

Create Database

POST /api/v1/databases

Body:

{ "name": "my-postgres", "type": "postgres", "project_id": "prj_abc123", "connect_site_ids": ["<site-uuid>"] }
FieldTypeOptions
namestringAlphanumeric, hyphens allowed
typestringpostgres, mongodb, valkey
versionstring?One of the engine’s versions from List Engine Versions. Omitted → the newest
project_idstringRequired. Project that owns the database
tierstring?Compute tier, e.g. xs, s, m. Omitted → the catalog default for that engine (see below)
disk_gbint?Disk to reserve, in GB. The volume is created at exactly this size, the size you are billed for. Omitted → the tier’s default disk
size_mbint?Legacy volume size in MB, used only when disk_gb is omitted
backup_tier_slugstring?Backup schedule, e.g. daily, sixhourly. Omitted → the catalog’s default backup tier
replica_setbool?MongoDB only, best omitted: MongoDB always runs as a single-node replica set. false is refused (400 standalone_mongodb_retired)
modestring?Valkey only: cache or store. Omitted → cache. Ignored for other engines. See Cache or store
connect_site_idsstring[]?Site uuids of the project to connect the new database to, at level connect. Omitted or empty → connected to no site

Every sizing field is optional; sending none of them creates the smallest database the engine supports, priced accordingly.

The default tier is engine-aware. With tier omitted, a database lands on the smallest tier that allows its engine. PostgreSQL lands on the smallest active tier (xs today), and MongoDB on the smallest MongoDB-capable tier — m on the current catalog, because the two smallest tiers cannot run it. Valkey lands on the smallest tier whose valkey_enabled is true in the catalog (GET /api/v1/marketplace/catalog) — every tier, xs included, today. A database is never placed on a tier that cannot run its engine: an explicit tier that cannot is refused with 400 rather than quietly upgraded — for Valkey, valkey is not offered on this tier: choose a tier other than "xs" (the slug is the one you sent).

project_id is required and permanent: a database belongs to that project for its whole life, and no endpoint moves it to another one. Its connection variables are derived for every site connected to it (see Site Access), so there is no attach call to make afterwards:

EngineVariables
PostgreSQLDATABASE_URL
MongoDBMONGODB_URI (alias MONGODB_URL)
ValkeyREDIS_URL and VALKEY_URL, both redis://c_<id8>:<password>@<host>:6379/0. Runtime only: a build never receives them

The tier’s points cost is reserved at creation. A create that would push the project past its plan budget answers 409, and one the platform has no capacity for answers 503 — see Points Troubleshooting.

Response: 201

{ "database": { "id": "…", "name": "my-postgres", "status": "provisioning" }, "connections": { "connected": [], "pending": ["<site-uuid>"], "failed": [] } }

connections says what became of each site in connect_site_ids:

FieldHolds
connectedSite ids connected now
pendingSite ids waiting for a new PostgreSQL or Valkey database to accept connections — usually within a minute. They connect once it does; until then the database lists them in pending_connections
failed{ "site_id", "error" } for each site that could not be connected. The database is created all the same, and error is a sentence you can show users

Every site id is checked before anything is provisioned: a site of another project answers 400, and a site you cannot write — a collaborator restricted to other sites — answers 403. Either way nothing is created. Nothing connects a database you did not choose a site for — see How a service gets connected.

Engine refusals:

StatuscodeWhen
400use_valkeytype is redis: “Redis is not offered. Create a Valkey database instead: it speaks the Redis protocol and every Redis client works with it.”
400valkey_disabledValkey is switched off on this platform: “Valkey is not available yet”
400invalid_modemode is not cache or store: mode must be "cache" or "store"
400standalone_mongodb_retiredtype is mongodb and replica_set is false: “MongoDB always runs as a single-node replica set now: a standalone instance cannot give each app its own database user. Leave replica_set out (or set it to true).”
400—version is not one the engine offers: unsupported version '9.0' for valkey, with supported_versions listing the ones it does
400—The tier does not run this engine (mongo_enabled or valkey_enabled is false on it)

List Engine Versions

GET /api/v1/databases/versions

The engine versions a create accepts in its optional version field, newest first; a create without one gets the first.

Response: 200

{ "mongodb": ["8", "7", "6"], "postgres": ["18-alpine", "17-alpine", "16-alpine", "15-alpine", "14-alpine"], "valkey": ["9.1"] }

List Databases

GET /api/v1/databases

Get Database

GET /api/v1/databases/:id

Response: 200 — { "database": { … } }.

FieldTypeMeaning
typestringpostgres, mongodb or valkey
statusstringprovisioning, running, stopped, error or resizing
status_messagestring?Why the database is in error — a Valkey that never became ready. Absent otherwise
valkey_modestring?Valkey only: cache or store. Absent for other engines

On a Valkey, version is 9.1 and port is 6379. A database body carries no username: a database’s own login is never shown to anyone.

Two fields describe sites still waiting for a new database to accept connections. Both are absent when no site waits:

FieldTypeMeaning
pending_connectionsstring[]?Ids of the sites that connect once the database accepts connections
connect_errorstring?Why they still haven’t connected, once they have waited 15 minutes

Delete Database

DELETE /api/v1/databases/:id

Get Credentials

GET /api/v1/databases/:id/credentials

An app reads its own credential here; a person never does. Called with a site’s managed key (the GHAYMA_API_KEY a connected site receives), this route answers with that site’s own connection credential — the one in its DATABASE_URL, MONGODB_URI or REDIS_URL:

Response: 200

{ "type": "postgres", "host": "pg-mypostgres-a1b2c3d4-0.databases.svc.cluster.local", "port": 5432, "username": "c_3f1a2b3c", "password": "<password>", "database": "db_mypostgres", "internal_url": "postgresql://c_3f1a2b3c:<password>@pg-mypostgres-a1b2c3d4-0.databases.svc.cluster.local:5432/db_mypostgres", "level": "connect", "credential": "connection" }

username is the connection’s own (c_ followed by eight characters), level is the connection’s level, and credential is always "connection". Three refusals:

StatuscodeWhen
403not_connectedThe key’s site is not connected to this database
403site_key_requiredA project-wide key — one you created, tied to no site — has no connection of its own
409no_own_credentialThe site’s connection has no credential of its own yet. The platform gives it one as soon as the database can (it retries every ten minutes); retry later, or reconnect the site

Called by a person — with a dashboard session or an account token — the route answers 410. A database’s own login is never shown, and each connected app already holds a credential of its own:

{ "code": "shared_credentials_retired", "error": "A database's own login is no longer shown to anyone: every app connected to my-postgres has its own credential in its variables. From your laptop: ghayma connect --local. For a system outside Ghayma: ghayma access add database my-postgres --name <principal>. Host, port and the variable names, without any password: ghayma db credentials my-postgres (CLI 0.14.0 or later)." }

On a Valkey the sentence has no For a system outside Ghayma part: a Valkey has no external principals yet.

The host and internal_url fields use internal Kubernetes URLs (.svc.cluster.local). Use these in apps deployed on Ghayma. To reach the database from outside the cluster, add a named principal — it is handed a URL on the database’s own <resource>.db.ghayma.tech host, with its own credential. That is the only external path. See the Networking Guide for details.

Site Access

Which of the owning project’s sites may reach this database over the network. Changes converge within ~2 minutes. The same rows decide which sites receive the database’s connection variables — a connected site gets them, a disconnected one loses them, on its running app and on every later deploy. On a PostgreSQL database, connecting a site also creates the role that site’s DATABASE_URL carries, and disconnecting it drops that role alone. On a Valkey it creates the user the site’s REDIS_URL carries, the same way.

These two routes are the per-database view of a connection at level connect. The project-wide Connections API reads and writes the same rows — for buckets and auth apps as well as databases — and is the only way to reach those other two kinds. Both keep working; use whichever fits the call you are making.

A new database is connected only to the sites in its create’s connect_site_ids — none, even in a project with a single site, when that list is empty — and stays deny-all until you call Set Site Access or Connect a Service.

List Site Access

GET /api/v1/databases/:id/sites

Response: every site of the owning project, flagged:

{ "sites": [ { "site_id": "…", "slug": "main", "name": "main", "has_access": true }, { "site_id": "…", "slug": "admin", "name": "admin", "has_access": false, "pending": true } ] }

"pending": true marks a site waiting for the database to accept logins; it connects once it does. A project with no sites yet returns { "sites": [] }.

Set Site Access

PUT /api/v1/databases/:id/sites

Body — the full desired set (an empty array revokes all access):

{ "site_ids": ["<site-uuid>", "<site-uuid>"] }

Returns the same shape as the list endpoint. 400 if any site_id belongs to a different project, and 403 unless you may write every site whose access the save changes. Saving also narrows the wait to the sites you saved: a waiting site you leave out no longer connects.

A Valkey that is not running yet answers 409 with code: "database_not_running": “the Valkey database is not running yet; connect the site once it is running (database cache is provisioning)”. Sites chosen at create connect on their own once it runs. Any other database that is stopped or still being provisioned answers 409 with code: "not_running", and one that cannot give an app a credential of its own answers 409 with code: "credential_unsupported": a new site is connected only with a credential of its own.

External Access (Named Principals)

A named principal is the only way to reach a database from outside. Each outside consumer gets its own credential, level, optional source allowlist and optional expiry. Reads need the project read role, every mutation write; :id is a project id or slug and :resourceId is the database’s UUID. Buckets use the identical routes under /buckets/.

RouteDoes
GET /projects/:id/databases/:resourceId/accessList the principals, revoked ones included. Never carries a secret
POST /projects/:id/databases/:resourceId/accessCreate one. 201 with the credential in the body — the only time it is readable
DELETE /projects/:id/databases/:resourceId/access/:accessIdRevoke it (204). Revoking twice is not an error
POST /projects/:id/databases/:resourceId/access/:accessId/rotateNew secret, same identity — returned once
PUT /projects/:id/databases/:resourceId/access/:accessId/allowlistReplace allow_cidrs with the whole list given; [] means any source

A create takes name (required), level, allow_cidrs and expires_at; omit any of the last three for the kind’s default level, any source, and no expiry. A 400 is a malformed name, level, CIDR or expiry; a 409 is a principal that is already revoked; a 503 is an engine that could not mint, and nothing was created. A project API key cannot call these routes at all — it is answered 403 before the handler.

A Valkey has no public access yet. A create on a Valkey answers 400, and its list is always {"access": []}:

{ "code": "public_access_unavailable", "error": "public access for this database engine is not available yet; use the ghayma tunnel" }

Reach it from your machine with ghayma connect --local.

See the External Access guide for the model and the hostnames it hands out.

Rotate Credentials

POST /api/v1/databases/:id/rotate

A database’s own login is never handed out, so it is not rotated from here: the route answers 410 for every engine.

{ "code": "shared_rotate_retired", "error": "A database's own login is no longer rotated from here: it is never handed out, and only an operator rotates it. To give one app a new credential: ghayma connections rotate database my-postgres --site <site>, or Rotate on the app's row in the console's Access list." }

To give one app a new credential, rotate its connection: POST /projects/:id/sites/:siteId/connections/database/:dbId/rotate, or ghayma connections rotate. The other apps keep theirs.

Stop / Start

POST /api/v1/databases/:id/stop POST /api/v1/databases/:id/start

Resize

PATCH /api/v1/databases/:id/tier

Changes a database’s compute tier, disk size, backup schedule, or any mix of them. Send only what changes; at least one field is required.

Body:

{ "tier": "m", "disk_gb": 10, "backup_tier_slug": "daily" }
FieldTypeMeaning
tierstring?New compute tier, e.g. s, m
disk_gbint?New disk size in GB, at least 1. Larger grows the disk; smaller shrinks it
backup_tier_slugstring?New backup schedule: weekly, daily, sixhourly

PATCH /api/v1/databases/:id/resize with {"storage_mb": 10240} changes the disk alone, rounded up to whole GB, under the same rules.

A new tier on a Valkey restarts it with the new memory limit: its maxmemory is 70 % of the tier’s memory limit. A Valkey disk grows online but cannot shrink yet.

A disk change finishes after the call returns. The 200 response already reads status: "resizing", and disk_gb keeps the old size until the disk has the new one; poll Get Database until status is running again. A grow happens online: the database stays up, and the new size applies in about a minute.

{ "database": { "id": "…", "name": "my-postgres", "status": "resizing", "disk_gb": 20, "disk_used_bytes": 1932735283, "min_disk_gb": 3, "resize": { "direction": "shrink", "target_disk_gb": 5, "phase": "copy_out" } } }

Every database object carries these disk fields:

FieldTypeMeaning
disk_gbintThe disk size it has now, in GB
disk_used_bytesintWhat the volume holds on disk: the data plus the engine’s own files, such as PostgreSQL’s WAL or MongoDB’s journal, so it is larger than storage_used_bytes. Read about every 10 minutes
min_disk_gbint?The smallest disk it can shrink to now. Absent until the first reading; when it is not below disk_gb, the disk cannot shrink yet. A Valkey disk cannot shrink whatever it reads
resizeobject?The change in progress, or the last one that failed. Absent otherwise
resize fieldMeaning
directiongrow or shrink
target_disk_gbThe size it is moving to
phaseThe step it is on: expand for a grow; stop, copy_out, release, recreate, copy_back, start, cleanup for a shrink, in that order
errorWhy the change failed. Present only after a failure

While status is resizing the database takes no other change: Stop, Start, Delete, a Valkey mode switch and any other resize answer 409 with code: "resize_in_progress", and Restore Backup is refused until it finishes.

While an operator converts a MongoDB database to a replica set, Stop, Start, Delete and both resize routes (PATCH …/tier and PATCH …/resize) answer 409 with code: "conversion_in_progress": “this database is being converted to a replica set (since …); try again when it has finished”.

The new size is billed once the change is done. A grow is checked against your points budget when you ask for it; a shrink lowers the price from the moment it finishes. See Pricing by Service.

Errors:

StatusCodeMeaning
400—disk_gb below 1 GB
409disk_too_smallThe data does not fit in 75 % of the new size. The body carries min_disk_gb, the smallest size that fits now, with disk_used_bytes and target_disk_gb
409resize_in_progressA resize is already running. Try again when it has finished
409database_not_runningThe disk changes only while the database runs. Start it first
409shrink_unsupportedA smaller disk on a Valkey, on both endpoints: “this database’s disk can grow, but shrinking it is not available yet”
503storage_capacityThe platform does not have the storage for this disk right now. Try a smaller size, or later

A points rejection (409 over budget, 503 platform capacity) is the same as for a create — see Points Troubleshooting.

Shrinking a disk

A PostgreSQL or MongoDB disk can shrink if what the volume holds fits in 75 % of the new size; min_disk_gb is that rule applied to disk_used_bytes. The call measures the volume again before it starts, so a shrink is checked against the current figure, not the last reading.

A shrink stops the database for about a minute on a small database, longer on a large one, while its files are copied to the smaller disk. Apps cannot connect during that time. Everything else stays exactly as it was: credentials, users, connection strings and data.

If a shrink fails before the old disk is released, it is undone automatically: the database comes back on its original disk with status: "running", and resize.error says what went wrong. If it stops later, status is error and the data is kept safe; support finishes the move.

Change Valkey Mode

PATCH /api/v1/databases/:id/valkey-mode

Switches a Valkey between cache and store — see Cache or store. Needs the project write role.

Body:

{ "mode": "store" }

Response: 200 — { "database": { … } } with the new valkey_mode.

The database restarts into the new mode: a few seconds of downtime. Before the response, a switch to store builds the append-only file, which takes longer on a large dataset, and a switch to cache takes a fresh snapshot. Once the database has started restarting, a cancelled request still records the new mode. Asking for the mode it already has changes nothing.

Errors:

StatuscodeMeaning
400—The body has no mode (request validation)
400not_valkey”this database is not a Valkey database”
400invalid_modemode must be "cache" or "store"
409database_not_runningOnly a running Valkey switches. The sentence is the shared disk one: “the database must be running to change its disk”
409resize_in_progressA resize is running. Try again when it has finished
500—The append-only file or the snapshot could not be written. The mode stays as it was

Database Logs

GET /api/v1/databases/:id/logs?tail=200&follow=1

The engine’s own log, for every engine: PostgreSQL, MongoDB and Valkey.

ParameterTypeDefaultDescription
tailnumber200Lines to return from the end of the log, clamped to 1-1000
followstring—1 keeps the response open and streams new lines. The platform ends it after 10 minutes

Response: 200 — text/plain; charset=utf-8, streamed. Each line starts with its RFC3339Nano timestamp:

2026-10-08T09:12:22.108356960Z 1:M 08 Oct 2026 09:12:22.108 * Valkey is starting

A follow also ends when the database restarts. Close the connection to stop it earlier. A follow may also carry empty lines that keep the connection open; skip them.

Reading the log needs the project write role. A project API key is refused with 403: “project keys cannot read database logs”.

StatuscodeMeaning
403—A project API key, or a role below write
409no_pod”the database has no running pod”: the pod does not exist — a stopped or new database — or its engine container has not started

Backups

A backup is a dump of the database itself. Roles you create inside the database (for example a least-privilege application user) and the privileges granted to them are not part of it, and a restore does not recreate them. Keep the statements that create your roles and grants with your migrations so you can re-apply them after a restore.

Create Backup

POST /api/v1/databases/:id/backups

List Backups

GET /api/v1/databases/:id/backups?page=1&per_page=20

Response: 200

{ "backups": [ … ], "total": 9, "page": 1, "per_page": 20, "has_more": false, "health": { "state": "healthy", "consecutive_failures": 0, "in_flight": false, "last_completed_at": "2026-09-24T09:27:49Z", "last_attempt_at": "2026-09-21T12:13:03Z", "next_attempt_at": "2026-09-28T12:13:03Z", "interval_hours": 168 } }

backups is one page, newest first. per_page is 20, 50 or 100; any other value falls back to 20.

health is the state of the database’s backup schedule, worked out from its most recent backups each time you ask:

FieldTypeMeaning
statestringnever (no backup yet), healthy, failing (scheduled backups are failing and the schedule is backing off) or paused (10 scheduled backups failed in a row and the schedule has stopped) — see Scheduled backups that keep failing
consecutive_failuresintScheduled backups that failed since the last backup that completed. Failed manual backups are not counted
in_flightboolA backup is running right now
last_errorstring?Why the latest scheduled backup failed. Present only while the schedule is failing or paused
last_completed_atstring?When the latest successful backup, scheduled or manual, was taken
last_attempt_atstring?When the latest scheduled backup was attempted, whatever the outcome
next_attempt_atstring?When the next scheduled backup is due. null while the schedule is paused
interval_hoursintHow often the schedule runs: 168 weekly, 24 daily, 6 every six hours

Scheduled backups that keep failing

A failed scheduled backup is not retried every hour. Each failure in a row makes the next attempt wait longer:

Scheduled backups failed in a rowNext scheduled attempt
11 hour after the failure
26 hours after
3 to 924 hours after
10None: the schedule is paused

Schedules are checked once an hour, so an attempt can start up to an hour later than the table says.

Take a manual backup to resume. Once the cause is fixed, call Create Backup. When that backup completes, consecutive_failures drops to 0, a paused schedule starts again, and the next scheduled backup is due one interval after the last successful scheduled one, or at the next hourly check if that time has already passed. A manual backup that fails does not count against the schedule. Manual backups count towards your plan’s limit on stored manual backups: at the limit, Create Backup answers 400, so delete an old manual backup first.

A failing schedule never costs you the backups you already have. Old scheduled backups are pruned only after a new scheduled backup succeeds, and manual backups are never pruned.

Restore Backup

POST /api/v1/databases/:id/backups/:backupId/restore

Restores a completed backup, scheduled or manual, into the database.

How a Valkey restore works. A Valkey backup is an encrypted snapshot. The restore stops the database, swaps the backup’s data in and starts it again: a few seconds of downtime. In store mode the old append-only file is dropped and a new one is built from the backup before the database accepts connections.

How a MongoDB restore works

A MongoDB restore never leaves a collection half-emptied:

  1. The backup is loaded next to your data first, into temporary collections named __ghayma_restore__<collection> in the same database. Your live collections are not touched while this runs.
  2. When everything is loaded, each temporary collection replaces its live collection in a single atomic step, one collection at a time. The backup’s views are then recreated.
  • If loading fails, your data is exactly as it was and the call returns the error.
  • If the swap fails part-way, which is rare, the error lists the collections that were restored and the ones that were not. The leftover temporary collections stay in the database until your next restore, which clears them before it starts.
  • Collections that are not in the backup are kept. A restore does not delete a collection just because the backup does not have it.
  • Not supported: time-series collections, which cannot be swapped in (the swap stops at the first one), and system.* collections. A recreated view keeps its source collection and pipeline but loses any collation set on it, and a collection with the same name as a view in the backup is replaced by that view.

Delete Backup

DELETE /api/v1/databases/:id/backups/:backupId