Managed databases for agents

This reference extends https://app.tokay.io/llms.txt with managed database operations for AI agents. Use these operations only when the user explicitly asks or a named readiness action requires them. Never rotate credentials, restore a snapshot, or replace data without user approval.

Last updated

AI agent? Start with llms.txt.

The resource and connection model

  • A project has at most one managed instance per engine (Postgres, MySQL, MongoDB, Redis), shared by its services. A resource is one physical engine, not one database.
  • Postgres, MySQL, and MongoDB resources contain logical databases Tokay knows about, such as app and auth. Each logical database has its own generated application username and versioned password. Changing only the database name in a connection string does not grant access to another logical database.
  • One serviceResourceConnection groups all env vars for one application connection and binds them to one logical database. An app that needs two databases receives two independent sets of env vars.
  • Redis credentials stay scoped to the whole resource because its numbered databases are not authorization boundaries.
  • Admin and read only platform credentials are never delivered to apps. The query endpoint uses the read only principal, while app tasks and migrations receive the service's ordinary scoped connection.
  • The query endpoint is read only. Data writes belong in a service task the user requested, and schema changes belong in release migrations.
  • A MySQL app that uses one login for SQL across schemas must split that access into separate connections. Static evidence blocks deploy with MYSQL_CROSS_DATABASE_CONNECTION_UNSUPPORTED, and MySQL 1044/1142 failures that correlate with that topology receive the same diagnosis.

Tokay still creates infrastructure automatically. Confirming a connection records the choice, but the engine is physically provisioned only during deploy.

What to call

To Use
List resources and logical databases project.resources
Run a read only query queryProjectResource
Export data createProjectResourceExportTicket, then GET the path
Stage an import upload createProjectResourceImportUploadTicket, then PUT the path
Import while binding a connection setServiceResourceConnectionLogicalDatabaseWithImport
Import into an existing logical database createProjectResourceImport
Poll, confirm, cancel, or retarget an import projectResourceImport, confirmProjectResourceImport, cancelProjectResourceImport, retargetProjectResourceImport
Rotate an application password rotateProjectResourceApplicationPassword
Return to an earlier password version activateProjectResourceApplicationPassword
List restore points projectResource.snapshots

Postgres extensions

Tokay's managed Postgres runs PostgreSQL 18.6. Normal migrations can install the extensions Postgres marks trusted, including common options such as pg_trgm, hstore, uuid-ossp, pgcrypto, ltree, and citext. Tokay also supplies and permits vector from pgvector 0.8.6 and postgis from PostGIS 3.6.4.

Extensions belong to one logical database. Add ordinary SQL to the migration that runs against the intended database:

create extension if not exists "vector";

Use the extension's exact name. The default target schema is public. Run the migration through the service's confirmed connection for that logical database so the extension installs where the app uses it. The resource query endpoint can inspect pg_catalog.pg_extension, but it is read only and is never an installation path. Other extensions that require superuser authority remain blocked.

Find resources and logical databases

query Resources($projectId: ID!) {
  project(id: $projectId) {
    resources(first: 10) {
      nodes {
        id
        resourceType
        runtimeName
        logicalDatabases(first: 100, orderBy: [NAME_ASC]) {
          nodes {
            id
            name
            runtimeUsername
            passwordVersions(first: 20, orderBy: [VERSION_DESC]) {
              nodes {
                id
                version
                isCurrent
                reconciledAt
                failedAt
                createdAt
              }
            }
          }
        }
      }
    }
  }
}

GraphQL IDs are Relay node IDs. Use projectResource.id in the query and export ticket, logicalDatabases.nodes[].id for password actions and imports that target an existing logical database, and a setup blocker's serviceResourceConnection.id when binding an application connection. A Redis resource has no logical databases. Read its password versions from projectResource.applicationPasswords.

Query

mutation Query($resource: ID!) {
  queryProjectResource(input: { projectResource: $resource, query: "select count(*)::int as n from tasks", logicalDatabaseName: "app" }) {
    records
    isTruncated
    truncationMessage
    retryAfterSeconds
  }
}

Query is for inspection. Export the database for bulk data. query is SQL for Postgres and MySQL, and JSON text for Redis (a command array such as ["GET","mykey"]) and MongoDB (a command or find object). logicalDatabaseName is optional for Postgres, MySQL, and MongoDB and must name a logical database Tokay knows about. Omitting it targets the contract default. Redis rejects database targeting.

Each record is the engine's row, document, or reply as a JSON string, in result order. At most first records (default 100, up to 1000) and 512 KiB return. When more existed, isTruncated is true and truncationMessage says how to read the rest: narrow or page the query (LIMIT and OFFSET, or limit and skip in MongoDB), shorten large values, or export. A single record over 512 KiB returns no records, so select a substring of the large column instead. A query the engine rejects is an error with reason QUERY_FAILED and the engine's message. Writes fail because the query uses a read only transaction or principal. Querying wakes a sleeping workspace. While the workspace or a newly created database starts, only retryAfterSeconds returns. Wait that long and repeat the call. MCP's query_database runs the same mutation and returns the records parsed.

Export

mutation Export($resource: ID!) {
  createProjectResourceExportTicket(input: { projectResource: $resource, format: NATIVE, logicalDatabaseName: "app" }) {
    projectResourceExportTicket { downloadPath expiresAt retryAfterSeconds }
  }
}

format defaults to NATIVE and logicalDatabaseName is optional. logicalDatabaseName scopes Postgres, MySQL, or MongoDB to one logical database Tokay knows about. Omitting it exports all known logical databases. Exports authenticate with each selected logical database's own credential, not a root secret. Redis rejects database targeting.

NATIVE preserves the engine backup shape and is the format imports accept back. A Postgres NATIVE export is a ZIP with one <database>.pgdump (pg_dump -Fc) per logical database. To restore one outside Tokay, run pg_restore --no-owner --no-acl -d <url> app.pgdump: the archive names Tokay's roles as owners, so a plain pg_restore lands every row but exits non-zero. CSV is supported for Postgres, MySQL, and MongoDB and returns a ZIP with one CSV per table or collection. Redis returns EXPORT_FORMAT_UNSUPPORTED_FOR_ENGINE.

Fetch {API_ORIGIN}<downloadPath> once without an Authorization header. The ticket expires quickly and works once. An export first wakes a sleeping workspace. While it starts, the mutation succeeds with a null downloadPath and a retryAfterSeconds delay. Wait that long and call it again. The wake request is kept, so retrying is safe and no export has started yet.

Import an existing database

Imports load an existing dump into one logical database per operation:

  • Postgres plain SQL, pg_dump custom format, or a Tokay native ZIP containing one database.
  • MySQL logical SQL or a Tokay native ZIP containing one database.
  • MongoDB mongodump --archive, compressed or uncompressed.
  • Redis is unsupported.

Dumps that span a whole cluster, define global roles, or contain several databases are rejected. Uploaded bytes never run through a managed admin connection. Tokay restores them in an isolated sandbox that runs the same engine and holds no secrets, normalizes the result, and proves a fresh restore under the application role before touching the managed engine.

Import step 1: stage the raw bytes

Request an upload ticket for the project, then PUT the raw dump to its path without a bearer:

mutation StageDump($project: ID!) {
  createProjectResourceImportUploadTicket(input: { project: $project }) {
    projectResourceImportUploadTicket { uploadPath maxBytes expiresAt }
  }
}
PUT {API_ORIGIN}<uploadPath>
Content-Type: application/octet-stream

<raw dump bytes>

The ticket expires after five minutes and takes one PUT, so request a new one to retry a failed upload. The HTTP 201 response contains stagedImportHandle, sha256, sizeBytes, and expiresAt. Staging only holds the bytes. It is not yet an import, and an unused blob expires after one hour.

Import step 2a: bind during onboarding

Resolve the normal UNCONFIRMED_LOGICAL_DATABASE blocker and attach the handle in the same mutation:

mutation BindDatabaseWithImport(
  $serviceResourceConnection: ID!
  $database: String!
  $handle: String!
) {
  setServiceResourceConnectionLogicalDatabaseWithImport(input: {
    serviceResourceConnection: $serviceResourceConnection
    logicalDatabaseName: $database
    stagedImportHandle: $handle
  }) {
    projectResourceImport {
      id
      state
      logicalDatabase { id name }
    }
  }
}

This transaction makes the connection and the import reference the same logical database. No later confirmation can send the app to a different one.

Import step 2b: target an existing logical database

mutation ImportExisting($database: ID!, $handle: String!) {
  createProjectResourceImport(input: {
    projectResourceLogicalDatabase: $database
    stagedImportHandle: $handle
  }) {
    projectResourceImport { id state }
  }
}

Import step 3: poll the durable import

query DatabaseImport($id: ID!) {
  projectResourceImport(id: $id) {
    id
    state
    step
    sourceFormat
    sourceSizeBytes
    normalizedSizeBytes
    expandedSizeBytes
    failure { action code message supportRef }
    logicalDatabase { id name }
    projectResource { id resourceType runtimeName }
  }
}
State Action
PENDING_VALIDATION, VALIDATING Poll. Tokay is validating and normalizing without changing active data.
AWAITING_DEPLOY Resume ordinary deploy readiness. Restore runs before release actions and app startup.
AWAITING_CONFIRMATION Existing data will be replaced. Explain the target and downtime, obtain explicit user approval, then confirm.
EXECUTING Poll. Tokay owns the maintenance window, rollback, and app recovery.
SUCCEEDED Done.
FAILED Stop and follow failure.action. Surface failure.message, failure.code, and failure.supportRef.
CANCELED, EXPIRED Stop. A new import needs a new upload.

Confirm only with explicit destructive approval:

mutation ConfirmImport($id: ID!) {
  confirmProjectResourceImport(input: { projectResourceImport: $id }) {
    projectResourceImport { id state step }
  }
}

The first call changes nothing and fails with CONFIRMATION_REQUIRED, carrying the target database and sizes in extensions.confirmation. Repeat it with confirmationToken added to the input to start the replacement.

cancelProjectResourceImport uses the same projectResourceImport input and is valid only before destructive execution. retargetProjectResourceImport takes projectResourceImport plus a destination logicalDatabase. Earlier normalization work may be reused when the new target runs the same engine, but target and capacity checks run again.

Validation enforces independent limits on upload size, expanded data, runtime, and disk capacity. Replacement blocks other maintenance on the resource while it runs, but stops only the services credentialed for the target logical database. The target keeps its stable name. Rollback protection lasts only until the old or new application is healthy again, and Tokay does not offer a delayed rollback that discards new writes.

Rotate and restore application passwords

Each ProjectResourceApplicationPassword is one version of an application password. Postgres, MySQL, and MongoDB have one per logical database, and Redis one for the whole resource. Rotate only on user request or known exposure:

mutation RotateDatabasePassword($database: ID!) {
  rotateProjectResourceApplicationPassword(input: {
    projectResourceLogicalDatabase: $database
  }) {
    projectResourceApplicationPassword {
      id
      version
      isCurrent
      reconciledAt
      failedAt
    }
  }
}

For Redis, pass projectResource: <resource id> instead of projectResourceLogicalDatabase. Rotation updates the engine first, then redeploys only the Services that use that credential, which apply the new password before they start. reconciledAt means both sides converged. If app recovery fails after engine rotation, Tokay automatically restores the latest previously reconciled version. failedAt marks the version that failed, and the Service's failure diagnosis explains why.

History moves only by activation. To return to an earlier version, read the versions and call activateProjectResourceApplicationPassword(input: { projectResourceApplicationPassword: <password version id> }) with the one you want. A failed version cannot be activated. Plaintext is never readable from the API.

Snapshots

query Snapshots($id: ID!) {
  projectResource(id: $id) {
    snapshots(first: 10, orderBy: [CREATED_AT_DESC]) {
      nodes { id createdAt reason storageBytes storageRemovedAt }
    }
  }
}

Release snapshots cover the managed resource. Import replacement uses its own rollback protection scoped to the target logical database during the maintenance window. A restore point is restored only together with the code that ran on it: rollbackService with restoreData: true (see the operations reference). It is a destructive user decision. Explain what current data will be lost and obtain confirmation first.

reason is RELEASE_REHEARSAL or PRODUCTION_RELEASE. Rehearsal snapshots are internal release protection rather than user restore points. Tokay keeps the newest 3 PRODUCTION_RELEASE restore points per database, with no age limit, and one taken before a release that dropped or rewrote data until its expiresAt, 7 days after the release. Any other older one keeps its storage only while a restore from it, the release that took it, or a maintenance hold still needs it; after that its row stays with storageRemovedAt set and it can no longer be restored.

Error shapes

You see It means Do this
RESOURCE_QUERY_DATABASE_NOT_FOUND / EXPORT_DATABASE_NOT_FOUND logicalDatabaseName names an unknown logical database List logicalDatabases, use an exact name
RESOURCE_QUERY_DATABASE_TARGET_UNSUPPORTED / EXPORT_DATABASE_TARGET_UNSUPPORTED logicalDatabaseName was sent for Redis Remove the target
PROJECT_RESOURCE_MAINTENANCE_ACTIVE An import replacement is running on that resource Poll the import, hold other maintenance until it finishes
DATABASE_IMPORT_UNSUPPORTED_ENGINE Redis import requested Stop. Redis import is not supported
DATABASE_IMPORT_*LIMIT* or HTTP 413 An upload or expanded data limit was exceeded Produce a smaller supported logical dump
DATABASE_IMPORT_SCHEMA_REMAP_UNSAFE A MySQL stored program uses the source database name ambiguously Import into that database name, or remove or rename the qualified reference or alias
failure.action: CHANGE_INPUT The uploaded artifact must change Show failure.message, fix the dump, and upload a new artifact
failure.action: RETRY Tokay stopped after exhausting automatic recovery Retry the import with a new staged upload
failure.action: RESOLVE_PREREQUISITE Workspace, target, consumer, or rollout state blocks the import Resolve the named prerequisite before retrying
failure.action: CONTACT_SUPPORT Tokay cannot safely recover through a user action Show failure.message, failure.code, and failure.supportRef
MYSQL_CROSS_DATABASE_CONNECTION_UNSUPPORTED One MySQL connection crosses schemas Split the code or config into separate connection env vars
QUERY_FAILED The engine rejected a read only query Fix the statement or the database target
404 on an already fetched downloadPath Export tickets work once Mint a new ticket
404 DATABASE_IMPORT_UPLOAD_TICKET_INVALID on PUT The upload ticket was already used or expired Request a new upload ticket