# Managed databases for agents

Query, export, and administer a project's managed databases through the API. This page is written for an AI agent. It extends the deployment protocol at `https://app.tokay.io/llms.txt`. Everything here is an escape hatch. The happy path needs none of it, so act on an explicit user request or a named readiness action, not on your own initiative.

## The resource model

- A project has at most one managed instance per engine (Postgres, MySQL, MongoDB, Redis), shared by every service in the project. A resource is one running engine, not one database.
- SQL engines hold named logical databases inside the one instance, such as `app` or `auth`. There is no bind mutation. Services bind to a logical database through confirmed env var wiring during deploy.
- The resource's default database is not where your app's tables live. Queries that omit `database` target the engine default, and `relation does not exist` there usually means the query ran without a `database` scope. The data is still where it was. Always pass the logical database name for SQL engines.
- The query endpoint is read only. Writes fail inside a read only transaction. Data fixes go through a one-off service task, whose environment holds real credentials. Schema changes go through migrations with a deploy.
- Connection values are versioned resource outputs. Rotation writes a new current version and old versions remain restorable, so a bad rotation is fixed with a rollback.
- An external database is not a resource. It arrives as an ordinary required secret holding the connection string.

## What to call

| To | Use |
|---|---|
| List resources and logical databases | `project.projectResources` |
| Run a read only query | `POST /resources/:id/query` |
| Export data | `POST /resources/:id/export_ticket`, then GET the path |
| See connection outputs and versions | `projectResource.resourceOutputValues` |
| Rotate a credential | `rotateResourceOutputValue` |
| Undo or redo a rotation | `rollbackProjectResourceOutput` / `redoProjectResourceOutput` |
| List and restore snapshots | `projectResource.projectResourceSnapshots`, `restoreProjectResourceSnapshot` |

## Find the resource

```
query Resources($projectId: ID!) {
  project(id: $projectId) {
    projectResources(first: 10) {
      nodes {
        id resourceType runtimeName runtimeDatabase
        logicalDatabases(first: 20, orderBy: [NAME_ASC]) { nodes { name } }
      }
    }
  }
}
```

The `id` here is the Relay node ID the REST endpoints below expect in their URL path.

## Query

```
POST {API_ORIGIN}/resources/:resourceId/query
{ "query": "select count(*)::int as n from tasks", "database": "app" }
```

`query` is a SQL string for Postgres and MySQL, a command array such as `["GET", "mykey"]` for Redis, and an object in command or find form for MongoDB. `database` applies only to SQL engines and must name a logical database Tokay knows about. The response streams one JSON object per row, each on its own line.

Failed queries still return HTTP 200 with an error row in the stream, `{"_tokay_error": "QUERY_FAILED", "message": "..."}`, so check the first line before treating output as rows. Envelope problems (an unknown `database`, extra keys, an oversized body) return HTTP 400 with the same shape. The transaction is read only. `INSERT` fails with SQLSTATE 25006, and the fix is a one-off task or a migration, never retrying the endpoint.

## Export

```
POST {API_ORIGIN}/resources/:resourceId/export_ticket
{ "format": "csv", "database": "app" }
```

Both keys are optional. The default format is `native`, meaning the engine's own dump form. CSV is supported for Postgres, MySQL, and MongoDB, and arrives as a ZIP with one CSV per table or collection. Redis has no honest CSV shape and returns `EXPORT_FORMAT_UNSUPPORTED_FOR_ENGINE`. `database` scopes a SQL export to one logical database, and omitting it exports all of them.

The response is `{"downloadPath": "/resource_exports/<ticket>"}`. `GET {API_ORIGIN}{downloadPath}` with no Authorization header streams the archive. The ticket works once and expires quickly. A second GET returns 404, so mint a fresh ticket per download.

## Rotate and restore credentials

```
query Outputs($id: ID!) {
  projectResource(id: $id) {
    resourceOutputValues(first: 20, orderBy: CREATED_AT_DESC) {
      nodes {
        id isCurrent version createdAt
        resourceOutputDefinition { id name isUserRotatable isUserRollbackable }
      }
    }
  }
}
```

Rotate only when the user asks or a credential is known to be exposed. Rotation restarts consumers and is never routine maintenance. Only definitions with `isUserRotatable: true` accept rotation. For a managed Postgres that means the password. Host, port, and URL are derived from it. Rotation takes the definition id rather than the value id.

```
mutation Rotate($projectResource: ID!, $def: ID!) {
  rotateResourceOutputValue(input: { projectResource: $projectResource, resourceOutputDefinition: $def }) {
    resourceOutputValue { id version isCurrent }
  }
}
```

Rotation creates a new current version and Tokay redelivers the dependent values to consuming services without any code change. `rollbackProjectResourceOutput` makes the previous version current again and `redoProjectResourceOutput` reverses the rollback, both with the same input shape. Stored values are never readable back through the API, so track versions by `version` and `createdAt`, not by content.

## Snapshots

```
query Snaps($id: ID!) {
  projectResource(id: $id) {
    projectResourceSnapshots(first: 10, orderBy: CREATED_AT_DESC) {
      nodes { id createdAt snapshotReason storageBytes storageRemovedAt }
    }
  }
}
```

Tokay snapshots a managed database immediately before a release action mutates it, and rehearsals may create their own temporary snapshots. Restore is a user decision, never a recovery you attempt on your own. It replaces current data, so summarize what will be lost and get the user's confirmation first, then call `restoreProjectResourceSnapshot(input: { projectResourceSnapshot: $id })`. A snapshot with `storageRemovedAt` set no longer holds restorable bytes.

## Error shapes

| You see | It means | Do this |
|---|---|---|
| HTTP 200 row `{"_tokay_error": "QUERY_FAILED", "message": ...}` | The engine rejected the statement | Read `message`, fix the query, mind the `database` scope |
| HTTP 400 `{"_tokay_error": "RESOURCE_QUERY_DATABASE_NOT_FOUND", ...}` | `database` names an unknown logical database | List `logicalDatabases`, use an exact name |
| `ERROR: cannot execute INSERT in a read-only transaction` | The query endpoint never writes | Use a one-off service task for data fixes, migrations for schema |
| `relation "x" does not exist` on a table you expect | Query ran against the engine default database | Pass `database` with the app's confirmed logical database |
| `EXPORT_FORMAT_UNSUPPORTED_FOR_ENGINE` | CSV requested for Redis | Use the default `native` format |
| 404 on a `downloadPath` you already fetched | Export tickets work once | Mint a new ticket |
