Skip to content

CLI commands

Complete reference for the apco CLI. The public npm package is apco-cli, and its installed executable is apco. Every command supports --json for machine-readable output: stdout is exactly one JSON envelope, and all human diagnostics and progress go to stderr. AI agents should always parse the --json envelope — see the agent contract.

bash
npm install -g apco-cli@0.1     # or: pnpm add -g apco-cli@0.1 / bun add -g apco-cli@0.1
apco --help

Global options

All commands accept these options:

OptionDefaultDescription
--manifest <path>apco.ymlManifest path
--jsonoffWrite machine-readable JSON (exactly one envelope on stdout)
--yesoffSkip confirmations for destructive operations
--verboseoffWrite verbose diagnostics
--quietoffSuppress human success output
--no-coloroffDisable color output

Project-targeting commands additionally accept --project <slug|id>. Without it, the project is resolved from the current directory's .apco/state.json (written by the first deploy), then from the manifest's explicit slug.

JSON envelope

Success:

json
{ "ok": true, "operation": "<command>", "warnings": [], "...": "command payload" }

Error:

json
{
  "ok": false,
  "operation": "deploy",
  "error": {
    "code": "CLOUD_UNAUTHORIZED",
    "message": "Invalid API key.",
    "hint": "Run `apco login` again or provide a valid APCO_TOKEN."
  }
}

Fields whose keys look secret (token, tokenRef, password, secret, apiKey, value, ...) are redacted to "[redacted]" in JSON output. See error codes for the full error.code catalog.

Exit codes

CodeMeaning
0Success
1Generic/cloud error (CLOUD_* except not-logged-in, DATABASE_NOT_READY, DEV_SYNC_UNAVAILABLE, UNEXPECTED)
2Usage error (CLI_INVALID_USAGE, CLOUD_NOT_LOGGED_IN, CONFIG_*, TOKEN_*, MANIFEST_*, ENV_*)
4Deployment failed or timed out (DEPLOY_*)
5Verification (health check) failed (VERIFY_FAILED)
6Destructive confirmation required (missing --yes)

Exit codes 3 and 7 were Coolify-era codes and are intentionally retired.

Authentication

Credentials resolve in this order: the APCO_TOKEN environment variable (CI/headless; takes precedence), then the stored login in ~/.config/apco/config.json. The stored tokenRef supports literal:VALUE, env:NAME, file:PATH, and dotenv:PATH:VAR schemes.

apco login

Device authorization flow: prints a verification URL + code on stderr, opens the browser, waits for approval, then mints a full-access API key named cli @ <hostname> and stores it in ~/.config/apco/config.json.

FlagDefaultDescription
--api-url <url>https://apco.spaceAPCO Cloud API URL (bare hosts are accepted and get https:// prepended)
json
{ "ok": true, "operation": "login", "warnings": [],
  "message": "Logged in to APCO Cloud as dev@example.com.",
  "apiUrl": "https://apco.space", "tokenRef": "[redacted]",
  "user": { "id": "6f0f3c1e-...", "email": "dev@example.com", "name": "Dev", "plan": "free" } }

apco logout

Revokes the CLI API key (best-effort) and clears the local login.

json
{ "ok": true, "operation": "logout", "warnings": [],
  "message": "Logged out of APCO Cloud and revoked the CLI API key.",
  "apiUrl": "https://apco.space", "revoked": true }

When not logged in the payload is { "message": "Not logged in to APCO Cloud.", "revoked": false }.

apco whoami

Verifies the active credentials against GET /api/v1/me.

json
{ "ok": true, "operation": "whoami", "warnings": [],
  "message": "Logged in as dev@example.com.",
  "apiUrl": "https://apco.space", "source": "config",
  "user": { "id": "6f0f3c1e-...", "email": "dev@example.com", "name": "Dev", "plan": "pro" } }

source is "env" when APCO_TOKEN is in effect, "config" when the stored login is used.

apco init

Creates an apco.yml manifest (only the manifest — no source files). Fails with MANIFEST_INVALID if one already exists (use --force).

FlagDescription
--name <name>App name (default: current directory name, sanitized)
--forceOverwrite an existing manifest
--template <template>Starter template: realtime-todo (type: ssr + managed database) or dockerimage (public unprivileged nginx on port 8080)
json
{ "ok": true, "operation": "init", "warnings": [],
  "message": "Created apco.yml.", "manifest": "apco.yml", "app": "my-app" }

With --template the payload adds the selected template and a safe hint. The dockerimage starter is a complete deployable manifest and creates no source files.

apco manifest validate

Validates apco.yml against the schema (see the manifest reference).

json
{ "ok": true, "operation": "manifest.validate", "warnings": [],
  "message": "apco.yml is valid.", "manifest": "apco.yml", "app": "my-app", "type": "ssr",
  "source": { "rootDirectory": "apps/web", "manifestPath": "apco.yml",
    "watchPaths": ["apps/web/**", "packages/shared/**"] },
  "release": { "configured": true, "timeoutSeconds": 300 } }

slug is included only when the manifest pins an explicit (non-auto) slug. On failure: MANIFEST_INVALID with the schema issues in error.hint, or MANIFEST_NOT_FOUND.

The repository-root manifest may select source.rootDirectory. An explicit nested --manifest apps/web/apco.yml selects that directory automatically and cannot retarget a different root. Deploy output and apco status --json expose the persisted root, manifest path, and watch paths under source. When a release command exists, validation returns only its safe configured/timeout summary and never echoes the command. Scheduled definitions are summarized without ever echoing their commands. For dockerimage, validation returns port and the canonicalized image.reference instead of source.

apco deploy

For source manifests, packs the current directory, uploads, and builds. For dockerimage, sends normalized JSON, pulls the registry image, and pins its immutable digest without packing source. Both paths wait until the deployment is running. When database.enabled: true, the ordinary resolve-or-create flow ensures the effective deployment-channel branch before admission. Forced --prod/--preview preflight that exact branch; an unforced deploy predicts from current deployMode but leaves the request unforced so the server remains authoritative.

The control plane's returned url is authoritative. Current CLI and MCP builds do not derive app hosts from the API hostname; derivation remains only as a fallback for older servers. Examples on this page use the hosted installation's current apco.space default.

FlagDescription
--yesSkip the project-creation prompt (recommended for automation)
`--project <slugid>`
--prodForce the Production channel
--previewForce the Preview channel and per-deployment URL

--prod and --preview are mutually exclusive. A direct --project deployment never provisions; its exact channel branch must already be ready or the server returns DATABASE_NOT_READY.

json
{ "ok": true, "operation": "deploy", "warnings": [],
  "message": "Deployed my-app to https://my-app.apco.space",
  "url": "https://my-app.apco.space", "channel": "production", "slug": "my-app",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app", "type": "ssr" },
  "deployment": { "id": "8a41f3aa-...", "status": "running",
    "release": { "configured": true, "status": "succeeded", "startedAt": "...",
      "finishedAt": "...", "exitCode": 0, "error": null, "skippedReason": null } },
  "source": { "bytes": 184320, "files": 42 } }

Failure modes: DEPLOY_TRIGGER_FAILED (archive over the size limit, upload rejected), DEPLOY_FAILED (build/start failed — exit 4), DEPLOY_TIMEOUT (no terminal status within 15 minutes — exit 4).

An OCI result replaces source with safe artifact provenance:

json
{ "ok": true, "operation": "deploy", "warnings": [],
  "url": "https://image-app.apco.space", "channel": "production",
  "project": { "id": "d2c1a9b4-...", "slug": "image-app", "type": "dockerimage" },
  "deployment": { "id": "8a41f3aa-...", "status": "running" },
  "artifact": { "kind": "oci",
    "requestedReference": "registry-1.docker.io/nginxinc/nginx-unprivileged:1.29-alpine",
    "digest": "registry-1.docker.io/library/nginx@sha256:...",
    "os": "linux", "architecture": "amd64" } }

OCI projects support Production/Preview but reject Dev. Promotion, Production rollback, and start recover the exact stored digest instead of resolving a tag. GitHub App source bindings reject dockerimage; GitHub Actions can invoke this CLI path. Registry credentials never appear in deploy output or logs.

For release.command, the same poll waits through the one-shot release phase. Its redacted output is part of apco logs; a failure returns DEPLOY_FAILED, keeps the previous route serving, and exposes the safe release summary through apco status --json / deployment polling.

Status/deployment responses expose only a command-free manifest read model. Build, start, release, scheduled-job, and dev commands plus env-file configuration are omitted; safe release/job metadata remains available.

Deployment statuses: queuedbuildingrunning | failed; a new deploy moves the previous production deployment to superseded; lifecycle adds stopped.

GitHub Actions integration

When GITHUB_ACTIONS=true, apco deploy records GITHUB_SHA, GITHUB_REF, GITHUB_REPOSITORY, and the pull-request number (APCO_GITHUB_PR_NUMBER, then the PR ref as fallback). For default PR checkouts the recorded SHA is GitHub's checked-out merge commit.

If GitHub's environment files are available, the command appends these step outputs without changing stdout:

OutputDescription
deployment_idAccepted APCO deployment id
channelResolved channel
statusTerminal result
release_statusOne-shot release result, when configured
dashboard_urlDeep link to the deployment
urlLive URL, success only

It also appends a success/failure section to GITHUB_STEP_SUMMARY, including the configured release result. JSON mode remains exactly one envelope on stdout; human mode continues streaming build logs.

apco registry

Manage project-scoped, write-only OCI registry credentials. These commands are valid only for dockerimage projects. Hosts are normalized and must be publicly routed; a credential matches only that exact host (and port).

CommandPurpose
apco registry list [--project ...]List host/configured/timestamps metadata only.
apco registry set <host> --username <name> [--project ...]Create or atomically rotate a host credential. Reads the password/token from a hidden prompt or stdin; there is no secret flag.
apco registry remove <host> [--project ...]Delete the exact host credential; destructive confirmation/--yes is required in JSON or non-TTY mode.
json
{ "ok": true, "operation": "registry.list", "warnings": [],
  "project": { "id": "d2c1a9b4-...", "slug": "image-app" },
  "credentials": [
    { "host": "ghcr.io", "configured": true,
      "createdAt": "2026-08-10T09:00:00.000Z",
      "updatedAt": "2026-08-10T09:00:00.000Z" }
  ] }

Usernames, secrets, tokens, and encrypted fields are never serialized. Use a read-only pull credential and feed automation from a secret manager over stdin.

apco rollback [deploymentId]

Re-deploys a previous Production or Dev deployment on the same channel. Without an id, the latest superseded Production deployment is used. Behaves like a deploy from there: polls (and streams logs in human mode) until running/failed. A historical release command is never replayed; the new deployment reports release.status: "skipped" with the rollback reason.

FlagDescription
`--project <slugid>`
json
{ "ok": true, "operation": "rollback", "warnings": [],
  "message": "Rolled back my-app to deployment 3f2b...: https://my-app.apco.space",
  "url": "https://my-app.apco.space",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app", "type": "ssr" },
  "rolledBackTo": { "id": "3f2b...", "finishedAt": "2026-07-06T21:14:03.000Z" },
  "deployment": { "id": "9c77...", "status": "running" } }

Preview deployments use promotion rather than rollback. An explicit superseded Dev deployment can be rolled back only to Dev.

apco promote [deploymentId]

Promotes a running Preview artifact to Production without rebuilding. This is code-only with respect to data: the new deployment uses the Production database, while Preview data remains untouched. If configured, its release command runs once for the new Production deployment against Production data before traffic switches. Failure keeps current Production serving and leaves the Preview live. If the manifest requires a managed database and Production is missing or unready, the command fails with DATABASE_NOT_READY; promotion never provisions or copies data. Without an id, the active running Preview is used.

apco stop / apco start

Stop the current production deployment (the URL serves a branded 503) or start a stopped one. Both queue a worker job (202) and poll until the deployment reaches stopped/running.

FlagDescription
`--project <slugid>`
--no-waitReturn once the action is queued instead of polling for completion
json
{ "ok": true, "operation": "stop", "warnings": [],
  "message": "Stopped my-app (stopped).",
  "url": "https://my-app.apco.space",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app", "type": "ssr" },
  "jobId": "b91e...",
  "deployment": { "id": "8a41f3aa-...", "status": "stopped" } }

With --no-wait the payload carries "queued": true and no deployment object. Lifecycle is production-only; dev-channel deployments are unaffected.

apco status

Shows recent deployments across Production, Preview, and Dev. The headline status/URL always describe Production; an active Preview is also returned separately.

FlagDescription
`--project <slugid>`
json
{ "ok": true, "operation": "status", "warnings": [],
  "message": "Project my-app: running.",
  "url": "https://my-app.apco.space",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app", "type": "ssr", "name": "my-app" },
  "deployments": [
    { "id": "8a41f3aa-...", "status": "running", "channel": "production", "error": null,
      "release": { "configured": true, "status": "succeeded", "startedAt": "...",
        "finishedAt": "...", "exitCode": 0, "error": null, "skippedReason": null },
      "createdAt": "2026-07-07T09:00:12.000Z", "startedAt": "2026-07-07T09:00:14.000Z",
      "finishedAt": "2026-07-07T09:01:02.000Z" },
    { "id": "3f2b...", "status": "superseded", "channel": "production", "error": null,
      "createdAt": "2026-07-06T21:13:00.000Z", "startedAt": "2026-07-06T21:13:02.000Z",
      "finishedAt": "2026-07-06T21:14:03.000Z" }
  ] }

apco logs [deploymentId]

Build log of a deployment (default: the latest deployment), or the running container's logs with --runtime.

FlagDefaultDescription
`--project <slugid>`
-f, --followoffKeep streaming until interrupted (Ctrl-C); incompatible with --json
--runtimeoffShow the running container's logs instead of the build log
--lines <n>200Runtime log lines to fetch per poll (server clamps 1–2000)
--since <value>Client-side filter: a duration (30s/15m/2h/1d) or an ISO timestamp — drop lines older than the cutoff
--grep <pattern>Client-side filter: show only lines matching a case-sensitive JS regex

Build log:

json
{ "ok": true, "operation": "logs", "warnings": [],
  "deploymentId": "8a41f3aa-...", "status": "running",
  "log": "[railpack] detected node app\n[build] ...\n" }

Runtime logs:

json
{ "ok": true, "operation": "logs", "warnings": [],
  "projectId": "d2c1a9b4-...", "runtime": true,
  "lines": ["2026-07-07T09:01:03Z Listening on :3000", "2026-07-07T09:02:11Z GET / 200"] }

--follow --json is rejected with CLI_INVALID_USAGE (exit 2) — poll apco logs --json on an interval instead. A deployment id is ignored with --runtime (runtime logs are per-project).

--since/--grep are applied client-side to build logs, runtime logs, and both --follow modes (in --json mode they filter the returned log/lines). Two caveats:

  • --since keeps untimestamped lines. Only lines whose parseable leading timestamp is older than the cutoff are dropped; a line without a parseable leading timestamp is always kept, so continuation/unlabelled output is never hidden.
  • Invalid values fail fast with CLI_INVALID_USAGE (exit 2) before any network call — a bad --since duration/timestamp or an unparseable --grep regex.

apco jobs

Inspect and control manifest-defined scheduled jobs. Project resolution follows the normal --project/state/manifest rules. Channel defaults to Production. Responses omit commands, image references, container identity, and environment values.

CommandPurpose
apco jobs lsList safe definitions and plan limits.
apco jobs run <name>Queue one immediate manual logical run (202).
apco jobs enable <name>Enable automatic scheduling for one definition.
apco jobs disable <name>Persistently disable automatic scheduling.
apco jobs runs [name]Cursor-paginated logical run history.
apco jobs logs <runId>Read redacted attempt logs.
apco jobs cancel <runId>Idempotently request cancellation.

Definition commands accept --channel production|preview|dev and --deployment <id>. A bare Preview filter targets the stable CLI/GitHub Actions Preview scope; pass an exact deployment id for a GitHub App PR Preview. runs additionally accepts --status, --limit 1..100, and --cursor. logs accepts --attempt, --since, --grep, and --follow; --follow is incompatible with --json.

json
{ "ok": true, "operation": "jobs.run", "warnings": [],
  "message": "Queued nightly-report run 91e2....",
  "project": { "id": "d2c1...", "slug": "my-app" },
  "run": { "id": "91e2...", "jobName": "nightly-report", "channel": "production",
    "trigger": "manual", "status": "queued", "attemptCount": 0 } }

The run id is stable across retries. Poll jobs runs --json or jobs logs <runId> --json; queue acceptance does not mean execution completed. See Scheduled jobs.

apco doctor

One-shot environment diagnostic — the fastest way for an agent (or human) to find out why a command isn't working. Runs six checks in order: auth, api, manifest, project, dev-channel, database. Later checks skip when a prerequisite failed (e.g. project skips if api failed).

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Database branch to check (default Production)
json
{ "ok": true, "operation": "doctor", "warnings": [],
  "message": "✓ auth — config (https://apco.space)\n\n\nhealthy",
  "channel": "production",
  "healthy": true,
  "checks": [
    { "name": "auth", "status": "pass", "detail": "config (https://apco.space)" },
    { "name": "api", "status": "pass", "detail": "dev@example.com" },
    { "name": "manifest", "status": "pass", "detail": "my-app (my-app)" },
    { "name": "project", "status": "pass", "detail": "my-app (d2c1a9b4-...)" },
    { "name": "dev-channel", "status": "pass", "detail": "no active dev deployment" },
    { "name": "database", "status": "pass", "detail": "connectivity ok (4ms)" }
  ] }
  • Always exits 0 — even when checks fail. Read the healthy boolean (and each check's status), not the process exit code. healthy is true only when zero checks are fail.
  • Each check's status is "pass" | "fail" | "warn" | "skip" and may carry a hint (e.g. a failing auth hints apco login).
  • No secrets in any detail — the API URL is included; tokens and connection strings never are. The database check runs a select 1 probe for the selected branch when ready and skips when that branch is absent.

apco verify

Polls https://<slug>.apco.space + the manifest's health.path (default /) every 3 seconds until it returns HTTP 200 or the timeout (health.timeoutSeconds, default 60) elapses.

FlagDescription
`--project <slugid>`
json
{ "ok": true, "operation": "verify", "warnings": [],
  "message": "Verified https://my-app.apco.space/.",
  "url": "https://my-app.apco.space",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "health": { "url": "https://my-app.apco.space/", "status": 200 } }

On timeout: VERIFY_FAILED, exit 5, with the last observed status in the message.

apco open

Prints (and, in human mode, opens in a browser) the deployed project URL. Resolves the slug from .apco/state.json, falling back to the manifest.

json
{ "ok": true, "operation": "open", "warnings": [],
  "message": "https://my-app.apco.space",
  "url": "https://my-app.apco.space", "project": { "slug": "my-app" } }

No browser is launched in --json mode.

apco admin nodes

Admin-only node discovery and lifecycle control. Always discover first with apco admin nodes list --json; show and mutations accept an exact node id or an exact unambiguous display name. Output excludes credentials, private keys, sealed task payloads, and tenant secrets.

sh
apco admin nodes list --json
apco admin nodes show <node> --json
apco admin nodes bootstrap-token create --file <new-path> --json
apco admin nodes cordon <node> [--reason <text>] --json
apco admin nodes uncordon <node> --json
apco admin nodes drain <node> [--reason <text>] --json
apco admin nodes cancel-drain <node> --json
apco admin nodes force-fence <node> --reason <text> --yes --json
apco admin nodes remove <node> --yes --json

bootstrap-token create writes the one-time value directly to a newly created mode-0600 file and never returns it in JSON or human output. Existing files are not overwritten. If the local write fails, the CLI attempts to revoke the unused server token before returning an error. The JSON envelope contains only the destination path and safe expiry/consumption metadata.

Force fencing revokes trust and routes before releasing capacity; it requires a bounded incident reason and destructive confirmation. Removal is accepted only after a node is drained to maintenance or force-fenced and has no live reservation or cleanup tombstone. A queued action returns queued, action, safe node identity/status, and an optional jobId.

apco env pull / apco env push

Pull the project's environment variables into a dotenv file, or replace the project's env with a dotenv file's contents. Values never appear on stdout — only key names. After env push, redeploy (apco deploy or apco rollback) for changes to take effect in SSR apps. See the environment variables guide.

FlagDefaultDescription
`--project <slugid>`
--file <path>.envDotenv file to write (pull) / read (push)
json
{ "ok": true, "operation": "env.pull", "warnings": [],
  "message": "Pulled 3 env var(s) into .env.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "file": ".env", "keys": ["API_URL", "DATABASE_URL", "NODE_ENV"] }

env push returns the same shape without file. A missing dotenv file fails with ENV_FILE_NOT_FOUND (exit 2).

apco projects

apco projects list

json
{ "ok": true, "operation": "projects.list", "warnings": [],
  "message": "2 project(s).",
  "projects": [
    { "id": "d2c1a9b4-...", "slug": "my-app", "name": "my-app", "type": "ssr",
      "url": "https://my-app.apco.space", "status": "running", "passwordProtected": false },
    { "id": "0b3d...", "slug": "docs", "name": "docs", "type": "static",
      "url": "https://docs.apco.space", "status": "stopped", "passwordProtected": true }
  ] }

status is the latest deployment's status (null when never deployed). Suspended projects additionally carry suspendedAt (ISO timestamp) and suspendReason ("quota" or "admin").

apco projects rename <newSlug>

Zero-downtime slug rename. Queues the rename, then waits (up to 60 s) until the new slug is live. The URL changes; .apco/state.json is updated when it points at this project.

FlagDescription
`--project <slugid>`
json
{ "ok": true, "operation": "projects.rename", "warnings": [],
  "message": "my-app.apco.space -> shop.apco.space",
  "url": "https://shop.apco.space",
  "project": { "id": "d2c1a9b4-...", "slug": "shop", "name": "my-app", "type": "ssr" },
  "oldSlug": "my-app", "newSlug": "shop" }

Slugs must be 3–30 chars of lowercase [a-z0-9-]; platform names and the dev- prefix are reserved.

apco projects delete <slug|id>

Destructive: deletes the project and all its deployments (and queues database + container/route teardown). Requires --yes in --json/non-TTY mode, otherwise fails with DESTRUCTIVE_CONFIRMATION_REQUIRED (exit 6). Removes the local .apco/state.json if it pointed at the deleted project.

json
{ "ok": true, "operation": "projects.delete", "warnings": [],
  "message": "Deleted project \"my-app\".",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" } }

apco db

Managed Postgres 17 or MariaDB 12.3 LTS commands (Pro plan). The first branch fixes one engine for the project; Production, Preview, and Dev are separate physical databases on that engine. Selected-branch commands default to Production and return channel and engine in JSON. See the database guide; the @apco/db SDK is Postgres-specific.

apco db enable

Provisions the Production database and polls until it is ready. This is a compatibility shorthand for apco db branch production.

FlagDescription
`--project <slugid>`
--engine <postgres|mariadb>Engine to lock in when this is the project's first branch (default Postgres)
--no-waitReturn once provisioning is queued instead of polling for ready
json
{ "ok": true, "operation": "db.enable", "warnings": [],
  "message": "Production database for my-app is ready.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "production", "engine": "mariadb",
  "databaseId": "5e9d...", "status": "ready",
  "sizeLimitMb": 512, "connectionLimit": 10 }

With --no-wait (or when already provisioned) the payload omits the size/connection fields. On a plan without databases the server rejects with PLAN_FEATURE_UNAVAILABLE. maxDatabases counts database-enabled projects: the first branch reserves one allowance, while Preview and Dev are bundled for that project. Later --engine values must match the immutable choice or the server returns DATABASE_ENGINE_CONFLICT.

apco db branch <production|preview|dev>

Idempotently creates an empty database for the selected deployment channel. It never clones another branch's schema or data. Accepts --project, --engine, and --no-wait, with the same polling and JSON fields as db enable; the operation is db.branch.

apco db status

Accepts --channel <production|preview|dev> (default Production).

json
{ "ok": true, "operation": "db.status", "warnings": [],
  "message": "Dev database for my-app: ready",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "dev", "engine": "mariadb",
  "enabled": true, "status": "ready",
  "sizeBytes": 1048576, "sizeLimitMb": 512, "connectionLimit": 10 }

When the selected branch is absent: "enabled": false, "status": null with zeroed/null usage fields. Never returns connection strings.

apco db disable

Destructive: queues one branch's drop. Accepts --channel (Production default) and requires --yes in --json/non-TTY mode. This does not stop its running deployment; completed backups remain available.

json
{ "ok": true, "operation": "db.disable", "warnings": [],
  "message": "Preview database drop queued for my-app.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "preview", "engine": "mariadb",
  "databaseId": "5e9d...", "status": "dropping" }

apco db migrate

Applies *.sql files in filename order, tracked in the target's _apco_migrations table. With explicit --channel, the CLI fetches that managed branch and ignores ambient DATABASE_URL. Without --channel, an ambient URL wins; otherwise Production credentials are fetched. It shells out to local psql or mariadb based on the URL scheme. Postgres wraps each file and tracking insert in one transaction; MariaDB preserves ordering in one session but cannot make arbitrary DDL transactional because it may commit implicitly.

FlagDefaultDescription
`--project <slugid>`
--channel <production|preview|dev>Force a managed branch; ignores ambient DATABASE_URL
--dir <path>.apco/migrationsMigrations directory
json
{ "ok": true, "operation": "db.migrate", "warnings": [],
  "message": "Applied 1 migration(s), skipped 2 already applied.",
  "target": "managed", "channel": "dev", "engine": "mariadb",
  "applied": 1, "skipped": 2,
  "files": ["003_add_index.sql"],
  "skippedFiles": ["001_todos.sql", "002_users.sql"] }

A managed result has target: "managed", channel, and engine. An unqualified ambient run has target: "environment" and intentionally omits channel, because APCO cannot infer the URL's target. A missing directory fails with CLI_INVALID_USAGE; a missing/failing engine client fails with DATABASE_NOT_READY.

apco db query [sql]

Runs arbitrary SQL against the selected branch as its tenant role. Unlike apco db migrate, this goes through the API—no local database client is needed.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Target branch (default Production)
--file <path>Read the SQL from a file instead of a positional argument
--dry-runPostgres only: run inside a transaction that is always rolled back

SQL source precedence is positional argument → --file → piped stdin; exactly one source is allowed (an argument together with --file, or no source at all, fails with CLI_INVALID_USAGE).

bash
apco db query "select id, text from todos order by id"
apco db query --file report.sql
echo "select count(*) from todos" | apco db query
apco db query --dry-run "update todos set done = true"   # executes, then rolls back
json
{ "ok": true, "operation": "db.query", "warnings": [],
  "message": "id  text\n--  ----\n1   ship docs\n\nrowCount: 1, durationMs: 3",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "preview", "engine": "postgres",
  "columns": ["id", "text"], "rows": [[1, "ship docs"]],
  "rowCount": 1, "truncated": false, "durationMs": 3, "rolledBack": false }
  • Rows are positionalrows is an array of arrays aligned to columns.
  • 500-row server cap. The server returns at most 500 rows; when it truncates, truncated is true and a results truncated to 500 rows by the server string is added to warnings. Narrow the query (add limit/where) to see the rest.
  • 100-row human cap. In human (non---json) mode the printed table shows at most 100 rows, followed by … N more rows not shown (use --json). --json always carries every row the server returned.
  • On Postgres, --dry-run sends rollback: true; the response reflects what would happen without persisting, and rolledBack is true. MariaDB rejects it with DRY_RUN_UNAVAILABLE because statements may commit implicitly. SQL errors come back as QUERY_ERROR (surfaced through CLOUD_REQUEST_FAILED). Never prints the connection string.
  • Scoped keys need database:query or projects:write.

apco db schema

Introspects the selected Postgres public schema or current MariaDB database. Accepts --channel (Production default); human output is a per-table summary and --json carries the full payload. Needs projects:read.

json
{ "ok": true, "operation": "db.schema", "warnings": [],
  "message": "1 table(s) in my-app:\n  todos  ~2 rows  16.0 KB  2 cols",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "production", "engine": "mariadb",
  "tables": [
    { "name": "todos", "approxRowCount": 2, "sizeBytes": 16384,
      "columns": [ { "name": "id", "dataType": "integer", "nullable": false, "default": null, "isPrimaryKey": true } ],
      "indexes": [ { "name": "todos_pkey", "definition": "..." } ], "foreignKeys": [] }
  ],
  "views": [] }

apco db tables [table]

Without an argument, lists the database's tables (name, ~rows, size). With a table name, shows that table's columns, indexes, and foreign keys from the schema payload; an unknown table fails with CLOUD_NOT_FOUND. Needs projects:read.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Target branch (default Production)
--countWith a table name, also fetch the table's exact count(*) (the summary otherwise shows the engine's catalog estimate)
json
{ "ok": true, "operation": "db.tables", "warnings": [],
  "message": "Table todos (16.0 KB, ~2 rows, 2 exact):\n  id integer NOT NULL PK\n  text text",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "dev", "engine": "mariadb",
  "table": { "name": "todos", "approxRowCount": 2, "sizeBytes": 16384, "columns": [ "..." ], "indexes": [], "foreignKeys": [] },
  "count": 2 }

count is present only with --count.

apco db usage

Live usage for one branch: size vs the per-branch plan limit, active/limit connections, the read-only flag, and up to 30 recent samples. Accepts --channel (Production default). Needs projects:read.

json
{ "ok": true, "operation": "db.usage", "warnings": [],
  "message": "Database usage for my-app:\n  size: 10.0 MB / 512 MB limit\n  connections: 2 / 10\n  read-only: no\n  last sample: 2026-07-10T00:00:00.000Z",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "dev", "engine": "mariadb",
  "sizeBytes": 10485760, "sizeLimitMb": 512, "readOnly": false,
  "connections": { "active": 2, "limit": 10 },
  "history": [ { "sampledAt": "2026-07-10T00:00:00.000Z", "sizeBytes": 10485760 } ] }

readOnly: true means only that branch is over the plan's per-branch dbSizeMb; writes to the other branches are unaffected. The plan's dbConnectionLimit is also per branch.

apco db migrations

Lists migrations in one branch's _apco_migrations table. Accepts --channel (Production default). Needs projects:read.

json
{ "ok": true, "operation": "db.migrations", "warnings": [],
  "message": "1 applied migration(s) for my-app:\n  001_todos.sql  2026-07-08T12:00:00.000Z",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "production", "engine": "mariadb",
  "tableExists": true,
  "migrations": [ { "name": "001_todos.sql", "appliedAt": "2026-07-08T12:00:00.000Z" } ] }

tableExists: false (with migrations: []) is a friendly "nothing has been migrated yet", not an error.

apco db backup list

Lists backups newest first. Defaults to Production; use --channel for one branch or --all-channels for project-wide discovery. Those flags are mutually exclusive. Every backup includes its recorded source channel and engine.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Source branch (default Production)
--all-channelsList Production, Preview, and Dev; result has allChannels: true and no singular top-level channel
json
{ "ok": true, "operation": "db.backup.list", "warnings": [],
  "message": "2 backup(s) for my-app across all channels:\n  …",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "allChannels": true,
  "backups": [
    { "id": "b1", "channel": "preview", "engine": "mariadb",
      "status": "complete", "sizeBytes": 1048576,
      "error": null, "filename": "my-app-2026-07-08.sql.gz",
      "createdAt": "2026-07-08T12:00:00.000Z", "completedAt": "2026-07-08T12:00:05.000Z" }
  ] }

apco db backup create

Requests a new engine-native backup (pg_dump or mariadb-dump, gzip-compressed) and—by default—polls the backup list until it is complete. Progress lines go to stderr in human mode.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Branch to back up (default Production)
--no-waitReturn once the backup is queued (status: "pending") instead of polling
json
{ "ok": true, "operation": "db.backup.create", "warnings": [],
  "message": "Backup for my-app is complete.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "preview", "engine": "mariadb",
  "backupId": "b1", "status": "complete", "sizeBytes": 1048576 }

Only one backup may be in flight per project, even across different channels. The plan's maxBackups is also a project-wide completed total. Errors identify the in-flight/source channel and direct you to backup list --all-channels.

apco db backup delete <backupId>

Destructive: permanently deletes the platform's copy of the backup (the live database is untouched). backupId is authoritative; optional --channel only asserts its recorded source. Requires --yes in --json/non-TTY mode.

json
{ "ok": true, "operation": "db.backup.delete", "warnings": [],
  "message": "Preview backup b1 deleted from my-app.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "preview", "engine": "mariadb",
  "backupId": "b1", "deleted": true }

apco db backup download <backupId>

Streams a completed backup (application/gzip) to a local file, written atomically. The file contents are never printed to stdout.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Optional assertion against the backup's source branch
--output <path>Destination file path, or an existing directory (the server's filename is written into it). Default: the server's Content-Disposition filename in the current directory.
json
{ "ok": true, "operation": "db.backup.download", "warnings": [],
  "message": "Backup b1 saved to /path/my-app-2026-07-08.sql.gz (1.0 MB).",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "preview",
  "backupId": "b1", "path": "/path/my-app-2026-07-08.sql.gz", "sizeBytes": 1048576 }

Downloading a backup that isn't complete (server code BACKUP_NOT_READY) surfaces as CLOUD_REQUEST_FAILED with a "wait for it to finish" hint. The download is written atomically and validated against Content-Length when the server sends it—a truncated transfer fails with CLOUD_REQUEST_FAILED and leaves no partial file. If --output names an existing directory, the file is written inside it under the server's filename. To restore, use apco db restore so the matching engine client is selected.

apco db rotate-credentials

Destructive: rotates one branch role's password immediately. Its running deployment loses access until redeployed/restarted; the other channels are unchanged. Accepts --channel (Production default), never prints the secret, and requires --yes in --json/non-TTY mode.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Branch to rotate (default Production)
json
{ "ok": true, "operation": "db.rotate-credentials", "warnings": [],
  "message": "Database credential rotation queued for my-app.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "dev", "engine": "mariadb",
  "status": "rotating",
  "note": "Your running deployment will lose database access until you redeploy or stop/start the project." }

apco db restore <backupId|file>

Destructive — overwrites your database

apco db restore overwrites the selected target branch. There is no undo. It requires --yes in --json/non-TTY mode. Promotion never does this automatically.

Restore a backup id or a local .sql/.sql.gz file into the project database. This is entirely client-side (there is no server-side restore endpoint): the CLI downloads the backup (or reads the local file), fetches the database credentials, and streams the dump into psql or mariadb based on the target URL. A local file must match the target engine.

FlagDescription
`--project <slugid>`
--channel <production|preview|dev>Destructive target (default Production)
--yesConfirm the overwrite (required in --json/non-TTY mode)

The argument is treated as a local file when it names an existing file (checked with fs.stat), otherwise as a backup id to download to a temp file (cleaned up afterward). .sql.gz files are gunzipped automatically — the CLI sniffs the gzip magic bytes (1f 8b), so a plain .sql file is piped straight through.

bash
apco db restore b1 --channel dev --yes      # copy backup b1 into Dev
apco db restore ./dump.sql.gz --channel preview --yes
apco db restore ./dump.sql --yes            # restore a plain SQL file
json
{ "ok": true, "operation": "db.restore", "warnings": [],
  "message": "Restored backup b1 into my-app.",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "channel": "dev", "engine": "mariadb", "sourceChannel": "preview",
  "source": "backup", "psqlExitCode": 0 }
  • Requires the matching client. The dump is streamed into psql -v ON_ERROR_STOP=1 or mariadb; a missing binary or nonzero exit fails with DATABASE_NOT_READY. The safe error tail is included, with the connection string redacted; MariaDB passwords are passed through MYSQL_PWD, not argv.
  • channel is always the target. A platform backup additionally returns sourceChannel; its ID, not the target, chooses the source. source is "backup" or "file". Credentials are never printed or returned.

apco domains

Custom domains for a project (Pro plan; per-project limit set by the plan). See the custom domains guide. All subcommands accept --project <slug|id>.

apco domains ls

Lists attached domains with status (pending | verified | failed), routingTarget (production | preview | dev | disabled), advisory dnsStatus (direct | cloudflare | unresolved | not-pointing), and — for unverified domains — the DNS records to create. Returns { domains, maxCustomDomains }.

apco domains add <domain>

Attaches a domain and starts background verification (up to 48 hours). Prints the TXT ownership record and the CNAME (edge.apco.space) or apex A record to create.

json
{ "ok": true, "operation": "domains.add", "warnings": [],
  "message": "Domain app.example.com added to my-app; create the DNS records to finish setup.",
  "project": { "id": "…", "slug": "my-app" },
  "domain": { "id": "…", "domain": "app.example.com", "status": "pending", "routingTarget": "production",
    "records": { "txtName": "_apco.app.example.com", "txtValue": "apco-verify=…",
      "cnameTarget": "edge.apco.space", "aRecordIp": null, "apex": false } } }

apco domains route <domain> <production|preview|dev|disabled>

Selects which current deployment channel serves the domain. Preview and Dev selections can be saved before the channel has a deployment and activate automatically after the next successful deploy. Disabled keeps the domain attached and verified but removes app routing, automatic www redirect, analytics attribution, and TLS eligibility.

json
{ "ok": true, "operation": "domains.route", "warnings": [],
  "message": "Domain app.example.com now routes to preview.",
  "project": { "id": "…", "slug": "my-app" },
  "domain": { "id": "…", "domain": "app.example.com", "routingTarget": "preview" } }

apco domains verify <domain>

Re-runs verification immediately and opens a fresh 48-hour window (use after fixing DNS or a failed status).

apco domains rm <domain>

Detaches the domain — it stops serving immediately. Destructive: requires --yes (or interactive confirmation).

apco dev

Hybrid local development: run the framework's dev server locally, optionally with cloud database credentials and/or a synced dev-channel deployment. See the hybrid dev guide.

FlagDescription
--dbEnsure the Dev branch and write its DATABASE_URL to .env.local (the selected database engine must be network-reachable from your machine)
--syncWatch for changes and deploy to the dev channel at dev-<slug>.apco.space (Pro; debounced 2 s)
--sync-onlyDeploy to the dev channel once, without starting a local server
--openOpen the local (or dev) URL in the browser after start
--port <n>Override the local dev server port (also settable via dev.port in the manifest)
`--project <slugid>`

Framework detection (from package.json dependencies, in order): Next.js, Nuxt, SvelteKit, Remix, Astro, CRA, Vite. Otherwise: the dev then start npm script (run with the detected package manager — npm, pnpm, yarn, or bun), then a bare index.html (served with npx serve .), then node .. A manifest dev.command overrides detection entirely.

Plain apco dev blocks until the local dev server exits:

json
{ "ok": true, "operation": "dev", "warnings": [],
  "message": "Dev server exited (code 0).",
  "framework": "vite", "port": 5173, "exitCode": 0 }

--sync-only is the only promptly-returning mode; it emits a deploy-style envelope:

json
{ "ok": true, "operation": "dev", "warnings": [],
  "message": "Synced to https://dev-my-app.apco.space",
  "url": "https://dev-my-app.apco.space",
  "channel": "dev",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app" },
  "deployment": { "id": "9c77...", "status": "running" },
  "source": { "bytes": 184320, "files": 42 } }

When the manifest enables a database, --sync/--sync-only ensure Dev before upload. Local --db and remote Dev therefore share Dev data without touching Production. There is no generic apco dev --channel; use apco db for explicit branch operations. Sync also requires a manifest, non-static project, and the plan's devSyncEnabled capability.

apco mcp

Runs the APCO MCP server on stdio for AI agent integrations, authenticated via the CLI login / APCO_TOKEN. It speaks the MCP protocol, not the JSON envelope — stdout is reserved for MCP, diagnostics go to stderr. See the MCP server reference for the tool catalog.

bash
apco mcp

APCO Cloud — ship apps with one command.