Skip to main content

SnoutData Cloud, for an agent

One page with every command, every flag, the output shapes, and the refusals. If you are a model reading this to provision a database, you need nothing else.

note

The snoutdata CLI is on npm. npx snoutdata downloads one bundled file with no dependencies and needs Node 20 or newer.

The whole thing in two commands

export SNOUTDATA_ACCESS_TOKEN=sdt_... # made once, by a person, with `snoutdata tokens create`
npx snoutdata init --env --json

That creates a hosted Postgres database, waits until it is actually serving, writes .snoutdata/project.json and .env, and leaves DATABASE_URL in place. It is idempotent: run it again in the same folder and you get the same project, not a second one.

A run holding nothing but an access token, with no browser and no human, has been driven end to end in 21 seconds.

The contract

--jsonAccepted on every command. Exactly one JSON value on stdout and nothing else. Progress, warnings and prose go to stderr, so piping into jq needs no filtering.
No promptsNothing asks a question when stdin is not a terminal. A command that would have to ask names the flag to pass and exits 2.
Unknown flagsAn error, never ignored. A typo stops the command instead of quietly doing something else.
Idempotenceinit reuses a linked project. db export treats a second request while one is running as the same request. tokens revoke on an already revoked token reports changed: false rather than failing.

Exit codes

CodeMeaningWhat to do
0Fine.Continue.
1The operation failed.Read the message on stderr. Do not retry blindly.
2The command was wrong.Fix the arguments. Retrying identically will fail identically.
3The credential is no good: never signed in, expired, or revoked. The message says which.Ask a human for a new access token.
4Not ready yet.Retry with a delay.
127psql, or pg_restore for an archive, is not installed.Install it, or use a path that does not need it.

Where the credential comes from

  1. SNOUTDATA_ACCESS_TOKEN
  2. ~/.snoutdata/auth.json, written by snoutdata login

The environment wins deliberately. Two kinds of credential go in that variable:

LastsUse it for
A session, written by loginone hour, refreshed automaticallya person at a terminal
An sdt_… access tokendoes not expire unless asked to; revocableCI, cron, an agent

The control plane exchanges an sdt_ token for a short-lived JWT on its side, so row-level security in the database is still the only thing deciding what it can see. A token cannot create another token. A token can revoke itself, or any other.

Where the project comes from

  1. --ref
  2. SNOUTDATA_PROJECT
  3. .snoutdata/project.json in this folder or any parent, walked upward the way git finds .git

A command that needs a project and cannot find one exits 2 saying so.

Every command

CommandFlags
snoutdata init--name NAME --region REGION --env --ref REF
snoutdata login--provider github (default google)
snoutdata logout
snoutdata whoami
snoutdata tokens list
snoutdata tokens create--name NAME (required) --expires DAYS
snoutdata tokens revoke <id|sdt_prefix>
snoutdata projects list
snoutdata projects create--name NAME (required) --region REGION --team NAME|ID --no-wait
snoutdata projects pause--ref REF
snoutdata projects resume--ref REF
snoutdata projects delete--ref REF
snoutdata teams
snoutdata link--ref REF (required)
snoutdata usage--ref REF --days N (default 30, max 365) --history
snoutdata db url--ref REF
snoutdata db psql--ref REF -- PSQL ARGS...
snoutdata db reset-password--ref REF
snoutdata db export--ref REF --out FILE --status
snoutdata db push--dir DIR (default migrations) --dry-run --out-of-order
snoutdata db restore--file DUMP (required) --ref REF --force
snoutdata mcp--allow-delete

Global on every command: --json, --help, --version.

What each command answers

init

npx snoutdata init --name analytics --env

Human output, on stderr:

Creating a project called "analytics"…
Created x8x3sb2hcx4xn.
Waiting for it to start…
creating
ready
Ready. x8x3sb2hcx4xn in us-west-2.
The password is stored for you: `snoutdata db url` prints this again.
Wrote DATABASE_URL in /work/analytics/.env.

The connection URL is the one thing on stdout. With --json you get the project, its password and the URI as one object.

whoami

{
"id": "9f3a…",
"email": "[email protected]",
"via": "token",
"token": { "id": "…", "name": "ci", "prefix": "sdt_wgygjk2r" }
}

via is "jwt" for a session and "token" for an access token. token is null for a session.

projects list

REF NAME STATE REGION LAST CONNECTION
x8x3sb2hcx4xn first light ready us-west-2 2 hours ago
ztwxsybkhpfee scratch paused us-west-2 9 days ago

A project over its storage limit shows read-only in the STATE column rather than ready, because that is the state that matters to whoever reads it.

With --json:

{
"projects": [
{
"ref": "x8x3sb2hcx4xn",
"name": "first light",
"region": "us-west-2",
"production": false,
"desiredState": "running",
"state": "ready",
"stateDetail": null,
"wakesInstantly": true,
"sizeBytes": 8074035,
"readOnly": false,
"readOnlyPending": false,
"readOnlySince": null,
"host": "x8x3sb2hcx4xn.db.snoutdata.com",
"database": "x8x3sb2hcx4xn",
"user": "x8x3sb2hcx4xn_owner",
"createdAt": "2026-09-04T11:02:19.417Z",
"lastConnectionAt": "2026-09-06T09:41:00.000Z",
"pausedAt": null
}
],
"allowance": {
"tier": "plus",
"used": 1,
"maxProjects": 5,
"maxStorageBytes": 8589934592,
"mayCreate": true
}
}

state and desiredState do not share a vocabulary. The settled form of a desired running is a state of ready. Poll state, not desiredState.

db url

Exactly one line on stdout, and it contains a live password:

postgres://x8x3sb2hcx4xn_owner:…@x8x3sb2hcx4xn.db.snoutdata.com:5432/x8x3sb2hcx4xn?sslmode=require

With --json: ref, host, port, database, user, password, ssl, uri, wakesInstantly, state.

If the project is over its storage limit, a warning goes to stderr, never stdout, so it cannot end up inside a .env line.

usage

first light (x8x3sb2hcx4xn)
7.7 MB of 8.0 GB on plus (0%).
Over 2 days: 1d 6h of compute, 2 connections.

With --history, the day-by-day table underneath:

DAY SIZE BACKUPS COMPUTE CONNS
2026-09-04 7.5 MB 41.2 MB 24h 1
2026-09-05 7.7 MB 41.9 MB 24h 1
2026-09-06 - - none 0

A dash is a day nothing measured it, which is what a paused project looks like. It is not zero. The current size is the most recent day that has a reading; compute and connections are summed, because there a missing day really did have none.

With --json the answer carries ref, name, readOnly, measuredAt, the days array (day, dbBytes, repoBytes, computeSeconds, connections, with dbBytes and repoBytes null on an unmeasured day), the limit (tier, maxStorageBytes), and a summary (days, latest, peakDbBytes, computeSeconds, connections, storage).

summary.storage.state is "ok", "near" (at or above 80%) or "over". That is the field to branch on.

tokens

PREFIX NAME STATE LAST USED EXPIRES
sdt_wgygjk2r ci live 3 hours ago never
sdt_4b1qz8mt laptop revoked 2 months ago 2026-12-04

tokens create prints the token on stdout, alone, so this is correct with no filtering:

SNOUTDATA_ACCESS_TOKEN=$(npx snoutdata tokens create --name ci)

It is shown once. Only its hash is stored.

teams

NAME ID SHARE
SolarPanda 2f0c9d1e-2b4a-4a77-9d1e-1c0a5b7d3f21 yes

A team you can see but cannot share into is listed with the reason in the SHARE column rather than hidden, because "your team is missing" sends somebody looking for a bug.

db push

npx snoutdata db push --dry-run
npx snoutdata db push

Runs every .sql file in the folder, in byte-wise name order, once each, recording what ran in a _snoutdata_migrations table in the project's own database. Order is ls | sort, so zero-pad (001-, 002-) or 10- sorts before 9-.

The ledger row is written in the same transaction as the migration, so a file that fails half way leaves nothing behind. A file that cannot be wrapped declares it:

-- snoutdata:no-transaction
create index concurrently people_email_idx on people (email);

Such a file gets no ledger row until it finishes, so an interrupted one runs again and has to tolerate that. db push says so before it runs one.

It refuses three things, reports every problem at once, and overrides only one:

RefusalOverride
A file that changed since it rannone
A file that has gone (usually a rename, which is how the same SQL runs twice)none
A new file that sorts before one already applied--out-of-order

Needs psql: a migration connects like any other client, because the control plane's SQL endpoint refuses DDL.

db export and db restore

npx snoutdata db export --out dump.sql # take a copy and download it
npx snoutdata db export --status # look, without starting one
npx snoutdata db restore --file dump.sql

--status exists so you can look without asking, because the other branch runs a pg_dump against a live database. A missing download link is not a failed export: links are signed with credentials that rotate every few hours, and asking again signs a fresh one against the same dump.

db restore sniffs the format from the file's first bytes rather than its name. It refuses a database that already has tables (--force overrides) and a project over its storage limit (--force does not). Errors about not owning pg_stat_statements when restoring one of our own exports are expected, are named as such, and do not fail the command.

MCP server

npx snoutdata mcp [--allow-delete]

An MCP server on stdin and stdout, speaking protocol version 2024-11-05. In a Claude Code MCP config:

{ "mcpServers": { "snoutdata": { "command": "npx", "args": ["snoutdata", "mcp"] } } }

It holds no credential of its own. It runs as whoever started it and every call is the same function the CLI makes, so it is exactly as capable as that person, minus deleting a database.

The eight tools

ToolArgumentsDoes
whoamiWho this is signed in as, and how.
list_projectsEvery database on the account, with ref, name, region, state, size, and whether it is read-only.
create_projectname (required), region, teamIdMakes a database and waits for it to be ready.
get_connection_urlref (required)The postgres:// URL. It contains a live password.
pause_projectref (required)Stops a project. Its data is kept and the next connection wakes it.
resume_projectref (required)Starts a paused project without waiting for a connection.
push_migrationsref (required), dir, dryRunRuns the .sql files in a folder. Needs psql.
delete_projectref (required)Destroys a database. Off unless --allow-delete.

Three things about it that will otherwise surprise you

The tool list is fixed at connect. MCP clients read tools/list once, so nothing is registered conditionally on state.

delete_project is listed even when it is off, and answers with a sentence about --allow-delete. A tool that is merely absent gets worked around with something invented; one that is present and says no can be reported back to the user.

A refusal is a result, not a protocol error. A quota, a paused project or a disabled tool comes back as a successful call with isError and a sentence, because a JSON-RPC error reads as "this tool is unusable" and stalls a client.

It does not run SQL

Deliberately. snoutdata sql does not exist yet, and when it does it will go through the engine's guard. An unguarded statement runner in an agent's hands is the thing that guard exists to prevent. An agent that wants to query calls get_connection_url and uses a real Postgres client.

Refusals you should expect, and what they mean

You will seeBecauseWhat to do
The plan allowance is used upFree 1 project, Plus 2, Pro 5Delete one, or tell the user to change plan
A free project cannot be marked productionA free project pauses after a week idle; Plus and Pro never pauseTell the user
Writes are refused, reads workOver the plan's storage limit, so the database is read-onlyShrink it, or tell the user to change plan
Connecting takes a few secondsThe project was paused and the connection is waking itWait. It is not an error
psql is not installed (exit 127)db psql, db push and db restore need itUse db url and a client you have

What does not exist

Do not invent these:

  • snoutdata sql
  • Branches, read replicas, larger compute sizes
  • Scoping an access token to particular projects
  • Any region other than us-west-2

The REST and GraphQL data API DOES exist, on paid plans only. A free project is refused it with a sentence about the plan. Auth, storage, realtime and edge functions are on every plan.

Also read