cli-reference.md
# InstaCloud CLI reference
Command catalog with flags and gates. Task guidance lives in `references/` — setup, deploy,
branching, governance, operate, mcp.
## Commands
| Command | Purpose |
|---|---|
| `insta login` [`--api-url <url>`] [`--env <prod\|staging>`] · `insta login --email <e> --password <p>` · `insta login --oauth <github\|google>` · `insta login --device` · `insta login --api-key <insta_…>` · `insta logout` | auth (api-url + tokens persist; tokens auto-refresh). **Bare `insta login` signs in from the browser** (any account type — email, GitHub, Google): it opens the console approval link locally, prints it as fallback, and polls until the human approves (~15 min window). `--device` is the same grant, print-only, for a machine with no usable browser (VMs, SSH, CI): the human approves **from a browser on any other machine**. `--oauth` opens a browser straight into the named provider (loopback capture). Agents use email/password or an API token; a password (`--password`/`$INSTA_PASSWORD`) needs `--email`. An `insta_` token can also come from ID-JAG registration at `POST https://api.instacloud.com/agent/auth` when the agent runs inside a participating provider (preview, see https://instacloud.com/auth.md), then `insta login --api-key insta_…`. `--env` targets a named deployment (see [Environments](#environments)); `--api-url` wins if both are given |
| `insta env` [`--json`] · `insta env use <prod\|staging>` [`--json`] | show or switch the deployment environment. **Switching drops the stored session** — prod and staging are separate deployments, so the old token cannot authenticate. See [Environments](#environments) |
| `insta status` [`--json`] | environment + login + linked project + current branch |
| `insta org list` [`--json`] · `insta org create <name>` [`--json`] | organizations (**one free org per user** — upgrade an existing org before creating another) |
| `insta project create <name>` [`--org <id>`] [`--json`] | create an **empty** project (no services), link this dir. With `--json`, a missing/unresolvable name is a hard error even on a terminal (never prompts) |
| `insta project list` [`--org`] [`--json`] · `insta project link <id>` [`--json`] | list / link existing |
| `insta project delete` [`--json`] | tear down ALL resources + unlink (gated: `project.delete`, approval by default) |
| `insta services add <postgres\|storage\|compute\|redis\|mysql\|mongodb> <name>` [`--branch <b>`] [`--region <r>`] [`--image <url>`] [`--port <n>`] [`--always-on`] [`--volume <gi>`] [`--json`] | provision a service **on a branch** (default: current/linked branch) — services are **branch-scoped**: adding one on a branch does not add it to any other branch; postgres/compute get a default access domain (gated: `service.add`). **compute only:** `--image` runs that image immediately at creation (otherwise compute starts as an empty, unreachable app until `insta deploy`); `--port` sets the listening port (default `8080`); `--always-on` creates it pinned-warm (never scales to zero — all plans, billing is actual usage either way); `--volume <gi>` attaches a persistent `/data` volume (also attachable later via `insta compute volume --size` — see [Volumes](#volumes)). The image is **persisted** on the service — shown in `services list`, re-run when the branch is forked, and updated later via `insta deploy --image`. Both positional arguments are optional in the parser: a **terminal** given **no type** picks from the same four kinds the dashboard's Add Service menu offers — **Docker Image** (asks for the ref, suggests a name from it, then the port), **Postgres** (`main-db`), **Storage** (`assets`), **Empty Service** (`compute`) — and is asked for a name after; given a **type but no name**, only the name is asked, for that type's plain kind. **Anything without a TTY errors and creates nothing** — with no type, all four kinds, each printed as the command to run instead (Docker Image as `insta services add compute <name> --image <ref> --port <n>`); with a type but no name, that kind's naming usage — so always pass both here. Docker Image is a *kind*, not a follow-up question: non-interactively it is just `--image` on a compute service. `--json` prints the created service object (id, type, name, domain, region, …) instead of the human line, and opts out of the prompts, so a missing positional errors the same way even on a terminal |
| `insta regions` [`--json`] | list regions available for postgres/compute services |
| `insta services list` [`--json`] [`--branch <b>`] · `insta services rename <type> <name> <new-name>` [`--json`] [`--branch <b>`] · `insta services remove <type> <name>` [`--branch <b>`] [`--json`] | list / rename / remove a branch's services (default: current branch; bindings keep pointing at renamed services; gated: `service.rename` / `service.remove`). `list --json` rows carry `pg_version` on postgres services (a platform field — any CLI passes it through; one per service row — a branch with several postgres services has one each) — the Postgres **major** the instance runs (e.g. `16`), known without waking it — so pick `pg_dump`/`pg_restore`/`psql` of the **same major** before you connect (a newer client's dump emits statements the server rejects, and a default restore continues past them). **(CLI ≥ 0.0.57)** the human line shows it as `pg 16`; `services add postgres` and `manifest` print the same badge. Rows older than the field were backfilled from the image every instance was born from, so if a restore still fails on version grounds against an old instance, confirm with `serverVersion`. `null` = a legacy row that never recorded one; key absent = a platform that predates the field. Either way, read the exact version instead, selecting the same branch and service as the DSN: `insta db stats --json --branch <b> [--group <g>]` reports `serverVersion` but never wakes a suspended instance (field present only while it runs), and `psql "$(insta db url --branch <b> [--group <g>])" -c 'show server_version'` answers in one step, waking it |
| `insta services secrets <type> <name>` [`--branch <b>`] [`--json`] | secret **names** bound to one service (e.g. `insta services secrets postgres db`) — default: current branch |
| `insta db url` [`--branch <b>`] [`--group <g>`] [`--json`] | print a postgres service's **connection string** (DSN). Default output is the bare URL alone on stdout, pipe-friendly (`psql "$(insta db url)"`); `--json` replaces it with a `{service, branch, url}` envelope. This is **the** command that yields the DSN: provider credentials are not in `insta secrets` (gated: `secrets.read`). `--group` picks one when the branch has several postgres services |
| `insta db connect` [`--branch <b>`] [`--group <g>`] | open an **interactive psql session** on a postgres service (needs `psql` on PATH; gated: `secrets.read`). A suspended instance wakes on connect — the first prompt can take a few seconds. Exits with psql's own exit code |
| `insta db stats` [`--branch <b>`] [`--group <g>`] [`--json`] | point-in-time **postgres stats snapshot**: connections vs the server's max (active count), cache hit rate, database size. Read-only; insta-db-backed services answer from the control plane without waking a suspended instance — `serverVersion` (rendered `PG 16.4`) is present only while the instance is running, like `cacheHitRatio`. Same read as the `insta_db_stats` MCP tool's `metrics` kind (its `insight`/`activity`/`query-stats` kinds are MCP-only) |
| `insta services scale compute <name> <number>` [`region`] | set the compute service's same-region replica count (**1–10**) — **paid plans only** (free → 403); gated: `service.scale`. `region` is an InstaCloud slug (e.g. `us-east`; see `insta regions`), **not** a raw Fly code |
| `insta compute limits [service]` [`--memory <size>`] [`--cpu <n>`] [`--branch <b>`] [`--json`] | show or set a compute service's **resource ceiling** — **any plan within the free cap; raising ABOVE the free cap needs a paid plan** (free → 403 beyond it; lowering is free on every plan). A new service is born AT its plan cap (free 4 vCPU / 4 GB, pro 8 / 8). Bare = read (prints the ceiling **and** the plan max). Setting **requires `--memory`** (`512mb`, `1gb` — decimal `mb/gb` and binary `Mi/Gi` suffixes both accepted); cpu derives from it, and `--cpu` is only an optional override for parallel workloads (never valid alone). Moves **both directions**: billing is actual usage, so the ceiling caps what the app may burn — it is not a price |
| `insta db limits` [`--cpu <n>`] [`--memory <size>`] [`--branch <b>`] [`--group <g>`] [`--json`] | same ceiling control for a postgres service — any plan within the free cap, paid above it; both directions. A new postgres is also born AT its plan cap (free 4 vCPU / 4Gi, pro 8 / 8Gi); its volume is the free cap on every plan. Takes provider quantities (`--cpu 2` or `2500m`; `--memory 4Gi`); either flag alone works. Bare = read the current ceiling |
| `insta compute volume [service]` [`--size <Gi>`] [`--delete`] [`--branch <b>`] [`--json`] | show, **attach**, grow, or **delete** a compute service's persistent `/data` volume. Bare = read (size, mount path, plan cap — any plan). `--size` on a volumeless service **attaches** one (any plan up to the default 10Gi, the free cap; mounts on the next deploy); on a volume-bearing one it grows — **paid plans, grow-only** (a provisioned disk cannot shrink). `--delete` **destroys the disk and ALL its data immediately** (any plan; irreversible; no detach exists — billing stops now, and suspend fast-wake + scale-out return; gated `service.remove`). See [Volumes](#volumes) |
| `insta db volume` [`--size <Gi>`] [`--branch <b>`] [`--group <g>`] [`--json`] | show or grow a postgres service's provisioned volume (block disk). Bare = read (size + plan cap — any plan); `--size` grows it — **paid plans, grow-only**. Postgres has its volume by default — there is nothing to attach |
| `insta storage list` [`--prefix <p>`] [`--cursor <c>`] [`--limit <n>`] [`--service <name>`] [`--branch <b>`] [`--json`] | list the objects in a storage service's bucket — one `size modified key` row each, in key order (S3 lists lexicographically). **Prefix filter only**: S3 has no substring search, so `--prefix` is applied **server-side** and there is nothing to match mid-key. Pagination is by cursor — the `nextCursor` a page prints is what `--cursor` takes; `--limit` is 1..1000 (default 100). `--service` picks one when the branch has several storage services (gated: `storage.read`) |
| `insta storage get <key>` [`-o <file>`] [`--service <name>`] [`--branch <b>`] [`--json`] | download one object. The platform returns a **short-lived presigned URL** (~60s) and the bytes come **straight from the provider** — nothing streams through the control plane. Writes to the key's **last segment** by default (never a path, so no key can write outside cwd); `-o` names the file. `--json` prints `{url, expiresAt}` and downloads nothing (gated: `storage.read`) |
| `insta storage delete <key>` [`--service <name>`] [`--branch <b>`] [`--json`] | delete one object — **irreversible**, and it runs immediately with no prompt (like every other destructive command here, the governance gate is the guard; a data operation stages nothing for a deploy). Deleting a key that is already gone still succeeds, so success is no proof it existed (gated: `storage.delete`, `allow` by default) |
| `insta services upgrade <compute\|postgres> <name> <spec>` | **legacy** (pre-usage-billing): raise a named spec, up-only — **paid plans only**; gated: `service.upgrade`. Prefer `insta compute limits` (and, for postgres, `insta db limits`), which also lower. **Compute-only in practice**: a postgres `upgrade` is rejected outright (400) — resize a postgres service with `insta db limits` (cpu/memory) and grow its disk with `insta db volume` |
| `insta branch create <name>` [`--from <parent>`] [`--json`] | isolated env: **forks the parent branch's current services** — a CoW database branch per postgres, a CoW-forked bucket per storage (snapshot-enabled projects), a clone of every compute service (re-running the parent's persisted image, if any) — then the two branches' service catalogs diverge independently (services are **branch-owned, not project-wide**). **≤10 branches/project.** Does NOT switch |
| `insta branch switch <name>` [`--json`] · `insta branch list` [`--json`] | set current branch / list |
| `insta branch merge <source>` [`--into <target>`] [`--json`] | **structural** merge: creates on the target branch (default: current) every service present on `<source>` but missing there — fresh & **empty, no data copied**. Services the target already has are skipped (reason: `exists`\|`cap`\|`secret-collision`). Additive only — never deletes target services; idempotent |
| `insta branch delete <name>` [`--json`] | tear down the branch's resources (gated: `branch.delete`) |
| `insta secrets` [`--branch <name>`] [`-o <file>`] [`--print`] [`--json`] | secret seam → write user-defined project/branch secrets to `./.env`; provider-minted service credentials are **not** exported here (gated: `secrets.read`) |
| `insta secrets list` [`--branch <b>`] [`--json`] | secret names for the branch, **grouped by service** — each service's bound secrets, plus a branch-level "unbound" group and a project-wide group |
| `insta secrets tree` [`--json`] | the whole project as `project → branch → service → secrets` (names only) |
| `insta secrets set <NAME> [value] [--branch <b>] [--service <compute/name>] [--json]` | Set a user secret (project-wide by default; value from stdin if omitted). `--service` scopes it to that branch's compute service (e.g. `compute/api`) — binding **requires a branch** (defaults to the current branch when `--service` is given); omit `--service` for an unbound secret (as before) |
| `insta secrets unset <NAME> [--branch <b>] [--json]` | Remove a user secret |
| `insta secrets sources` [`--branch <b>`] [`--json`] | List provider credential sources available for explicit compute binding, e.g. `postgres/db: DATABASE_URL` or `redis/cache: REDIS_URL, ...` (names only; gated: `secrets.read`) |
| `insta secrets bind <ENV_NAME> <source>` [`--source-name <name>`] `--to <compute/name>` [`--branch <b>`] [`--json`] | Bind one provider credential from `<source>` (`postgres/db`, `redis/cache`, `mysql/orders`, `mongodb/catalog`, `storage/assets`, …) into a compute service's runtime env var. `--source-name` is required when the source exposes multiple credential names. Takes effect on the next deploy — or immediately on a running service with `insta compute restart` (CLI ≥ 0.0.51) (gated: `secrets.write`) |
| `insta secrets bindings --target <compute/name>` [`--branch <b>`] [`--json`] | List provider credential bindings for one compute service (names only; gated: `secrets.read`) |
| `insta secrets unbind <ENV_NAME> --from <compute/name>` [`--branch <b>`] [`--json`] | Remove one provider credential binding from a compute service; takes effect on the next deploy — or immediately on a running service with `insta compute restart` (CLI ≥ 0.0.51) (gated: `secrets.write`) |
| `insta build [dir]` [`--explain`] [`--port <n>`] [`--json`] | **verify before you deploy** — local, offline, deploys nothing, needs no login: prints the detection plan (builder, install/build/start commands, port **with the reason it was chosen**, `.env.example` keys), the Dockerfile (yours, or — **if nixpacks is installed**, never auto-installed — the one nixpacks would generate; `--explain` includes its content), and static checks each with a next action (missing Dockerfile/start command, port mismatch, `node_modules` shipping in the build context). **Verdict semantics (CLI ≥ 0.0.48): only a Dockerfile IN the directory can make a dir `deployable`.** A dir with no Dockerfile where nixpacks detects the app gets `builder: nixpacks` but its Dockerfile check is a ⚠ warning and the verdict stops at `needs-attention` (exit 0) — because `insta deploy <dir>` builds the directory's own Dockerfile and refuses without one; the nixpacks lane is server-side, for GitHub-connected repos only. The nixpacks Dockerfile shown by `--explain` is **for inspection, not standalone** (it `COPY`s `.nixpacks/` support files the dir does not have) — do NOT save it as `Dockerfile`; use the detected install/start commands as the starting point for your own. Verdict `failed` (exit 1) = no Dockerfile and nixpacks missing/undetected, or no start command. Run it before `insta deploy <dir>` instead of finding out from a burned remote build |
| `insta deploy <dir>` / `--image <url>` [`--branch <b>`] [`--group <g>`] [`--port <n>`] [`--json`] | deploy to a compute service — a **source dir** (**requires a `Dockerfile` in the dir — there is no no-Dockerfile/nixpacks lane on this path**; without one it exits 1 naming the options: write a Dockerfile, `--image <url>`, or connect the repo on GitHub, whose server-side lane builds Dockerfile-less repos with nixpacks. On InstaCloud it builds the Dockerfile remotely on Fly — no local Docker; against a local insta-oss daemon the CLI builds with your local docker instead, same command) or a **prebuilt image**. Defaults to the branch's sole compute service; `--group` picks by name (gated: `deploy`). `--json` prints one `{image, machineId, url, branch, group, nextActions}` document on stdout — build progress moves to stderr so stdout stays parseable |
| `insta template list` [`--json`] · `insta template info <code>` [`--json`] | browse the platform **template registry**: one row per template (code / version / category / required-var count / deploy count / name — tagline), and the detail view — version, maintainer, source, upstream pin, a services summary (types, ports, volumes), and every required/optional variable with its description, generator or default |
| `insta template deploy <code\|./dir>` [`--branch <b>`] [`--set <NAME=value>`] [`-y`, `--yes`] [`--json`] | deploy a template's whole service set onto a branch (default: current) — a **registry code**, or a **local directory** carrying `insta.template.yaml`. A bare word is **always** a registry code; local mode needs a path-looking target (`./dir`, `/abs/dir`, `~/dir`, `sub/dir`), so a same-named directory in the working dir can never shadow a registry template. Missing required variables are prompted for on a terminal; `--yes` or no TTY fails with the exact `--set NAME=value` list instead. `secret:N`-generated and defaulted variables are resolved **by the platform** — generated secrets never transit. Renders the 4-step pipeline (create services → write variables → deploy → health check), then the per-service URLs; `--json` replaces all of that with one document. May come back `approval_required` (hint on stderr, envelope on stdout with `--json`, **exit 2** — as every gated command). See [Templates](#templates) |
| `insta compute set-domain <host>` / `check-domain <host>` / `remove-domain <host>` [`--branch --group --json`] | attach / check / detach a **developer-owned custom domain** on a compute service — Fly issues the cert + routes; prints the DNS records to set in **your own** registrar (set/remove gated: `deploy`) |
| `insta compute start\|stop\|suspend [service]` · `insta compute status [service]` [`--json`] | control a compute service's lifecycle — **persistent override** of auto scale-to-zero: `stop`/`suspend` take it offline and traffic will **not** wake it until `start`; `status` shows desired vs. live state. All plans; ungated. `[service]` defaults to the project's sole compute service |
| `insta compute restart [service]` [`--branch <b>`] [`--json`] | **(CLI ≥ 0.0.51)** **re-run the image reference the service already runs**, against a freshly resolved env bundle — it asks for no new version and no new spec, though it does **not pin a digest**: a service recorded against a moving tag (`app:latest`) gets whatever that tag resolves to now (source deploys record a unique label and are unaffected). The two reasons to use it: a secret/binding changed and the running app hasn't picked it up (env is baked into the machine at deploy time), or the machine is up but **wedged** (`start` no-ops on a machine that is already `started`). The service must be **running**: a deliberately stopped/suspended one 400s and points at `insta compute start`. A never-deployed service 400s like `exec` does. **A running machine is health-gated coming back up** — if the app doesn't answer on its port the machines are rolled back (best-effort) to the config they were serving and the failure is reported (that verdict means the app is broken, not the platform). Whether an **idle machine (the default) is woken and gated at all depends on the compute plane** (`insta manifest --json` names it per compute row — `fly`, `microvm`, or a neutral `compute` when the platform reported none) — the Fly-backed one hands it the new config without waking, so the command returns fast, bills no uptime, and proves nothing about whether the app boots. Send it a request if you need that proof; see [operate.md](references/operate.md). All plans; **gated: `deploy`** — it lands configuration the way a deploy does, so a policy denying deploys denies this too; `start`/`stop` stay ungated and cycle a wedged machine without one. Refused while the org is billing-suspended; any machine it wakes bills as ordinary uptime, an idle one it leaves asleep costs nothing. WebSocket concurrency **is** re-asserted (it is recorded on the service), so a socket app does not need a redeploy to stay one |
| `insta compute exec [service] -- <command> [args...]` [`--branch <b>`] [`--timeout <sec>`] [`--json`] | run a **one-shot** command on the service's live machine — no interactive shell, no stdin. Wakes a scaled-to-zero machine first (the wake counts as billed uptime). `--timeout` bounds the run, **1–180s** (default 30). The CLI's **exit code is the remote command's exit code** — safe for scripts/agents to branch on. stdout/stderr stream to their own local streams verbatim, each capped at **1 MiB** (truncation noted on stderr); `--json` returns the raw response instead of split streams. Gated on **both** `deploy` and `secrets.read` — a deny on either is a 403. A service with no image ever deployed 400s: "this service has no machines yet — deploy an image first, then retry" |
| `insta compute always-on on\|off [service]` [`--branch --json`] | idle-mode dial: `on` = machines never scale to zero (no cold starts; idle RAM bills at actual usage), `off` = default scale-to-zero (idle costs ~nothing, first request cold-starts). All plans; billing is actual usage either way |
| `insta db always-on on\|off` [`--branch --group --json`] | same dial for a postgres service: `off` (default) suspends the idle instance — first connection after idle cold-starts; `on` keeps it warm |
| `insta manifest` [`--json`] | agent-legible env view: each branch's db / storage / compute + URLs. Database resources carry `ref.pgVersion` (`--json`; the Postgres major, root and branch rows alike) and **(CLI ≥ 0.0.57)** a `pg 16` badge on the printed line |
| `insta metrics <db\|compute\|redis\|mysql\|mongodb>` [`group`] [`--branch --from --to --step --json`] | service metrics (compute + managed DBs=Fly, full; db=provider-limited) |
| `insta logs <db\|compute\|redis\|mysql\|mongodb>` [`group`] [`--branch --limit --region --instance --deploy --json`] [`--from <t>`] [`--to <t>`] [`--since <dur>`] | logs (compute + managed DBs=Fly; db=provider-limited). **Without a window, one recent provider page (~100 lines) comes back regardless of `--limit`** (the provider cursor only pages forward) — the answer says so in a `note` when cut. `--from`/`--to` (unix seconds or ISO-8601) or `--since` (`90s`/`30m`/`2h`/`1d`) page a time window (~7-day retention, up to 5000 lines). `--deploy` shows **deploy events** (Fly machine lifecycle: created/started/…) instead of runtime logs — any Fly-backed target, not db; window flags don't apply to it |
| `insta usage` [`--from --to --json`] | usage aggregated by meter, with `costUsd` (snapshotted at collection) |
| `insta billing` [`--org <id>`] [`--json`] | current cycle summary: tier / included credit / used / overage / status |
| `insta billing upgrade <pro\|team>` · `insta billing portal` [`--org`] [`--no-open`] [`--json`] | Stripe Checkout to subscribe / Customer Portal to manage (opens a browser; `--no-open` prints the URL) |
| `insta events` [`--branch <b>`] [`--limit <n>`] [`--json`] | audit + agent-event timeline |
| `insta policy get` [`--json`] · `insta policy set <action> <decision>` [`--json`] | view / set governance policy (actions include `service.add/remove/rename/scale/upgrade/setAccess` and `storage.read` / `storage.delete`) |
| `insta approvals list` [`--status`] [`--json`] · `insta approvals approve <id>` [`--always`] [`--json`] · `insta approvals deny <id>` [`--json`] | manage gated actions |
| `insta observe install` · `report` [`--json`] · `sync` | local credential-audit hook (see below) |
| `insta feedback --type <bug\|feature-request\|friction\|other> --component <cli\|mcp\|platform\|skills\|docs\|other> --title <t> --detail <d>` [`--file <path>`] [`--area <a>`] [`--command <c>`] [`--error <e>`] [`--expected <x>`] [`--workaround <w>`] [`--doc <ref>`] [`--severity <blocker\|major\|minor>`] [`--json`] | report an **InstaCloud-side** hurdle to the team (see [Feedback](#feedback)) — never for the user's own app. Works logged-out/unlinked/oss; ungated. Non-TTY with missing flags errors (never prompts); transport failures **exit 0** — continue the task, don't retry |
| `insta upgrade` · `insta autoupdate [on\|off]` | self-update the CLI (binary re-runs the installer; npm uses `npm i -g`). Auto-update is **on by default** pre-1.0; `autoupdate off` / `INSTA_NO_AUTOUPDATE=1` disables. (CLI ≥ 0.0.5) |
| `insta setup agent` [`--env <prod\|staging>`] [`--mcp-token`] [`--project <id>`] [`--create [name]`] | one-step agent onboarding: **self-installs the CLI globally first** when running from the npx cache with no durable `insta` on PATH (`npm i -g insta@<running version>`; best-effort — a failed install prints the manual fallback and setup continues; CLI ≥ 0.0.37), making `npx -y insta@latest setup agent` a complete cross-platform one-liner (bash / zsh / PowerShell / cmd); then installs the insta skill user-globally for every coding agent, then registers the **remote MCP server** — Claude Code via `claude mcp add` (user scope) plus a config-file entry for every other detected MCP-capable agent. Default = **OAuth**, no credential written (browser auth on first `/mcp` use); `--mcp-token` = headless fallback that mints a durable `insta_` token named `mcp-<hostname>` (needs `insta login`; Claude Code only). Idempotent; `INSTA_MCP_URL` / `INSTA_SKILLS_REPO` override the URL / skill source. **Environment (CLI ≥ 0.0.38): bare `setup agent` always targets prod** — a machine previously switched to staging is switched back (persisted via the `env use` path, foreign session dropped, announced). Staging is the explicit form `--env staging` (also persists the switch); `$INSTA_ENV` counts as explicit; a deliberate custom host (`$INSTA_API_URL`, or a persisted custom apiUrl) is left alone unless `--env` is given. The skill source, the MCP host and its registration name all resolve from that one environment, so a staging setup installs `InsForge/insta-skills#devel` and registers `insta-cloud-staging` → `mcp.staging.instacloud.com` (both environments can coexist on one machine). Ends with a status-aware `next:` hint (login / project create / the prompt.md fetch prompt). **`--project <id>` (CLI ≥ 0.0.48) also links the working directory to that project** after setup — same code path as `insta project link <id>` (writes `./.insta/project.json`, installs per-project stack skills + the observe hook), flowing through the interactive login offer first when there's no session. With no session available (declined login, `-y`, non-TTY) the link is skipped with the manual `insta login` + `insta project link` hint (exit 0); a failed link (bad id / no access) exits 1. This is the flag behind the console Connect panel's single-line CLI setup. **`--create [name]` (CLI ≥ 0.0.52) instead creates a NEW project** and links it — same code path as `insta project create`, with the same login flow, skip-with-hint and exit-1-on-failure behaviour as `--project`; the name is optional and resolves exactly as that command's does — a directory with no usable name (`~`, `~/projects`, `/tmp`) gets that command's guidance and no project, not an error. `--create` and `--project` are mutually exclusive, rejected before anything is installed. See [mcp.md](references/mcp.md) and [Environments](#environments) |
| `insta mcp install` [`--agent <claude-code\|cursor\|codex\|opencode\|copilot\|factory-droid>`] [`--mcp-token`] | register the remote MCP server only (no skill install) — default: Claude Code + all detected agents; `--agent` targets one. Config merges never clobber existing entries; restart the tool afterwards |
**`--json` contract (CLI ≥ 0.0.37):** every mutating command an agent chains from takes `--json` —
one JSON document on stdout, progress/diagnostics on stderr, so `$(insta … --json)` always parses.
Deliberate exceptions: `insta run` has no `--json` (its stdout belongs to the child command — its
"injected secrets" banner is on stderr); `insta setup agent` and `insta upgrade` don't have it yet
(their stdout is a live installer stream).
**Approval gate exit code (CLI ≥ 0.0.37):** when a gated command returns `approval_required`, the
hint prints to **stderr** and the CLI **exits 2** — distinct from 1 (error), so scripts/agents can
branch on "approvable: have an admin `insta approvals approve <id>`, then re-run". Previously this
printed to stdout and exited 0, which read as success in pipelines.
Provider-minted credentials are **per-branch and per-service**. They live under the service that
created them with canonical names (`DATABASE_URL`, `REDIS_URL`, `MYSQL_URL`, `MONGODB_URL`,
`AWS_ACCESS_KEY_ID`, `BUCKET_NAME`, …), and **do not automatically appear** in `insta secrets`,
`insta run`, or a compute deployment. Decide what each compute service should receive with explicit
bindings:
```bash
insta secrets sources
insta secrets bind DATABASE_URL postgres/db --to compute/app
insta secrets bind REDIS_URL redis/cache --source-name REDIS_URL --to compute/app
insta secrets bindings --target compute/app
insta deploy . --group app --port 8080
```
If a source exposes exactly one credential (`postgres` → `DATABASE_URL`), `--source-name` is
optional. If it exposes several (`storage`, `redis`, `mysql`, `mongodb`), pass the source credential
name to bind. Binding overwrites the target env var's previous binding; an env name that collides
with a user secret visible to the same compute service is rejected (409). Binding itself does not
expose plaintext — the one CLI read that does is `insta db url` / `insta db connect` (the postgres
DSN, gated `secrets.read`); every other credential value only runs where it is bound (the deployed
app, or `insta compute exec`).
Changes apply on the next deploy — **or on `insta compute restart`** (CLI ≥ 0.0.51), which re-runs
the image reference the service already runs against a freshly resolved bundle. There is still no hot reload:
either way the machine takes a new config and restarts on it, in place (the machine id survives). An
idle machine may take the config without waking — see the `insta compute restart` row and
[operate.md](references/operate.md). A project may have **multiple services of every type**, up to
`INSTA_MAX_SERVICES_PER_TYPE` (default 5) per type.
`insta secrets set <NAME>` / `unset <NAME>` manage **user-defined** secrets. A user secret cannot
collide with a provider credential binding visible to the same compute service. Gated:
`secrets.write`. Changes apply on the next
`insta secrets` fetch, the next deploy, or an `insta compute restart` (CLI ≥ 0.0.51) — no hot reload either way. `--service` on `secrets set` scopes a
user-defined secret to a branch compute service; it is separate from provider credential binding
(`secrets bind`). `secrets list`, `secrets tree`, `services secrets`, `secrets sources`, and
`secrets bindings` are all **names only**.
## Volumes
A **volume** is a service's persistent block disk — always a **service attribute**, never a
standalone resource (nothing to create or list separately; it lives and dies with its service):
- **postgres** has one by default — view/grow only: `insta db volume` [`--size <Gi>`].
- **compute** opts in at creation (`insta services add compute <name> --volume <gi>`) **or any
time later** (`insta compute volume <name> --size <gi>` on a volumeless service attaches one;
the disk mounts on the **next deploy**). Fixed mount path **`/data`** (survives deploys and
restarts). Constraints: machine count stays **1** (scale to 1 before attaching), idle
scale-to-zero uses **stop** (cold wake) instead of suspend, and a volume **never detaches** —
but it **can be deleted** (`insta compute volume <name> --delete`): the disk and **all its
data** are destroyed immediately (irreversible — download anything you need first), billing
stops, and both constraints lift. View/grow/delete: `insta compute volume` [`--size <Gi>`]
[`--delete`].
- **Any plan may attach up to the default 10Gi** (the free cap — the platform's sizeless default on every plan, insta-platform #327, kept by #378); viewing is every plan. Only **growth is paid** and
plan-capped — don't pre-check the plan, just run the command: the backend's 403 carries the
upgrade hint. **Grow-only** — a provisioned disk cannot shrink.
- Billing is **actual data stored**; the size is a cap, not a price.
## Environments
`prod` and `staging` are **separate deployments** — different regions, databases, and auth. A
session minted by one can never authenticate against the other, so `insta env use` drops the stored
session and you log in again. Default is `prod`; nothing changes unless you switch.
| | `prod` (default) | `staging` |
|---|---|---|
| control plane | `api.instacloud.com` (us-east-2) | `api.staging.instacloud.com` (us-west-1) |
| MCP server | `mcp.instacloud.com/mcp` | `mcp.staging.instacloud.com/mcp` |
| registers as | `insta-cloud` | `insta-cloud-staging` |
| agent skills | `InsForge/insta-skills` | `InsForge/insta-skills#devel` |
| CLI channel | latest stable release | newest prerelease (`v*-rc.N`), else stable |
Install one-liners — each installs a complete stack for its environment (control plane, MCP
registration, and skill text all match). The npx form works on **every OS and shell** (Node 18+;
CLI ≥ 0.0.37 self-installs globally); the curl form is the no-Node native-binary path for
macOS/Linux only — **never run it on native Windows**, where PowerShell's `curl` alias and the WSL
`bash` shim break it:
```bash
npx -y insta@latest setup agent # production (any OS — ALWAYS prod, CLI >= 0.0.38)
npx -y insta@latest setup agent --env staging # staging (any OS; persists the env switch)
npx -y insta@latest setup agent --project <id> # + link this directory to a project (CLI >= 0.0.48; the console Connect panel's one-liner)
npx -y insta@latest setup agent --create [name] # + create a NEW project (CLI >= 0.0.52; name defaults to this directory)
curl -fsSL agents.instacloud.com | sh # production (macOS/Linux, no Node needed)
curl -fsSL agents.staging.instacloud.com | sh # staging
```
Staging via npx is the `--env staging` one-liner above (CLI ≥ 0.0.38) — it persists the env switch
itself, so no separate `insta env use staging` is needed (and a bare `setup agent` afterwards would
switch the machine back to prod, by design). The npm route always installs the **stable** CLI
build; staging's prerelease channel is the curl installer's concern (npm's `next` tag can lag
behind `latest`, so `insta@next` is only for deliberately testing a prerelease newer than stable).
> **`agents.staging.instacloud.com` is not live yet** (its DNS/CloudFront ships separately). Until
> it resolves, use the raw URL, which is exactly what the short host will serve:
>
> ```bash
> curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh -s -- --agents --staging -y
> ```
>
> If the environment cannot be applied (a CLI older than `insta env`), the installer **fails with a
> non-zero exit** rather than silently leaving you on production.
```bash
insta env # current environment + its hosts
insta env use staging # switch; persisted to ~/.insta/config.json
insta login --env staging # or --email/--oauth as usual
```
Control plane, MCP host **and** skill source are resolved **together** from one switch, so this
machine's CLI and its agents can never end up on different environments — including the case where
the CLI talks to staging while the agent reads prod's skill text.
Resolution order, most specific first:
1. `INSTA_API_URL` — a literal URL; the only way to reach a host no environment name covers
(`insta-oss` on localhost, a preview deployment). `INSTA_MCP_URL` and `INSTA_SKILLS_REPO` do the
same for the MCP host and the skill source.
2. `INSTA_ENV` — `prod` | `staging`. An unrecognised value is an **error**, never a silent fallback
to prod.
3. the persisted `apiUrl` in `~/.insta/config.json`.
4. `prod`.
Prereleases never take the `latest` GitHub release or npm's `latest` dist-tag (they ship
`--prerelease` and under npm `next`), so a staging CLI build can't reach production installers.
**Agents:** don't switch environments as a debugging step. If a command fails, check `insta env`
first — targeting staging when the user meant prod (or vice versa) produces confusing "project not
found" errors, because the two have entirely separate project lists.
## Deploy
```
insta build ./app --port <n> # first: verify — `deployable` only with a Dockerfile IN ./app
insta deploy ./app --port <n> # build ./app's Dockerfile remotely on Fly, push, deploy
# no Dockerfile → exits 1 (no nixpacks lane here); write one, use --image, or connect the repo on GitHub
insta deploy --image <url> --port <n> # or deploy a pre-built / already-pushed image
# targets the CURRENT branch's sole compute service (or --group <name> when there are several);
# --branch targets another branch; the URL prints on success.
# Source mode needs the `fly` CLI (auto-installed via Homebrew on macOS) but NO Fly login — the
# platform mints a short-lived, app-scoped deploy token for the build.
```
`--port` must match the port the image actually listens on (`ENV PORT` / `EXPOSE` / server bind) — a
mismatch boots fine but every request fails with `instance refused connection on 0.0.0.0:<port>`.
At deploy, compute receives `PORT`, user-defined secrets visible to that compute service, and
provider credentials you explicitly bound with `insta secrets bind`. It does **not** receive every
platform credential by default. Read env vars from `process.env` in production; **never bake
`./.env` into the image**. A compute service serves one app on one port at `https://<app>.fly.dev`.
**Custom domain (bring your own):** `insta compute set-domain app.example.com` attaches your own
domain to a branch's compute service and prints the DNS records to add **at your registrar** (a
`CNAME → <app>.fly.dev` for a subdomain, `A`/`AAAA` for an apex, plus a validation `CNAME`). The cert
(Let's Encrypt) + routing are handled for you; `insta compute check-domain <host>` shows status once
DNS propagates. The domain's DNS lives in your zone — you set it, not InstaCloud.
> Multiple services of every type (postgres/storage/compute, up to 5 each), `insta services
> scale`/`upgrade`, and **source-directory deploy** (`insta deploy <dir>` → Fly remote builder of the dir's own Dockerfile, no
> local Docker) are all implemented.
## Dockerfile templates
Moved to [references/deploy.md](references/deploy.md) (backend / full-stack / SPA patterns).
## Templates
A **template** is a whole service set (images, ports, volumes, env) published as one unit —
`insta template deploy <code>` creates those services on a branch, writes their variables, deploys
and health-checks them, instead of a hand-rolled `services add` + `secrets set` + `deploy` sequence.
- **Two modes, chosen by the target.** A bare word (`plausible`) is **always** a registry code. A
path-looking target (`./plausible`, `/srv/tpl`, `~/tpl`, `sub/dir`) is **always** a local directory and must
contain `insta.template.yaml` (validated locally, then sent inline). So a local directory never
shadows a registry template — `./` is how you opt into the local one.
- **Variables.** `--set NAME=value` (repeatable) answers them up front. Required variables with no
answer are prompted for on a terminal; with `--yes` or no TTY the command fails listing exactly
what to pass. Variables carrying a `secret:N` generator or a default are resolved **by the
platform** — generated secrets never leave it.
- **Outcomes.** `succeeded` prints each service's URL — then run `insta secrets` to refresh `./.env`.
`partial` is **terminal**: the healthy services stay up and the created resources are kept, so read
the log tail, then re-run the deploy to retry or `insta services remove <type> <name>` to clean up.
## Feedback
`insta feedback` reports a hurdle in the **InstaCloud toolkit itself** to the InstaCloud team.
File it when InstaCloud got in *your* way, then **continue the user's task with a workaround** —
never block on the report, and never file feedback for problems in the app the user is building.
```bash
insta feedback --json --type bug --component cli --area deploy \
--title "deploy --branch deploys to main" \
--detail "insta deploy --branch feat accepted the flag but the release landed on main" \
--command "insta deploy . --branch feat --port 8080" \
--error "deployed ... (branch main)" \
--expected "release lands on branch feat, per this reference" \
--doc "skills/insta/cli-reference.md" --workaround "insta branch switch feat, then deploy"
```
Situation → `--type`:
- **"This should work (per docs / the stated contract), but doesn't"** → `bug`.
- **"I was instructed to do X, but reality required Y"** → also `bug`, **with `--doc` +
`--expected` + `--workaround`** — you can't know whether the instructions are stale or the
product regressed, and those three fields let the team disambiguate.
- **"What I need is not supported"** → `feature-request`.
- **"Works, but confusing or awkward"** → `friction`.
`--component` is which piece of the toolkit (`cli|mcp|platform|skills|docs|other`); `--area` is
the product domain (deploy, branch, secrets, db, storage, compute, governance, billing, …). Free
text is **redacted locally** (tokens, emails, home paths) before sending and re-scrubbed
server-side; repeat reports of the same title within a week **fold into one record**
(`status: duplicate`). Project/org/branch context, CLI version, and OS attach automatically when
available. Via MCP: the `insta_feedback` tool takes the same fields (plus explicit
`projectId`/`branch`).
## Govern & observe
- **Policy** gates `secrets.read`, `secrets.write`, `deploy`, `branch.delete`, `project.delete`,
`storage.read` (`storage list` + `storage get`), `storage.delete`, and
`service.add` / `service.remove` / `service.rename` / `service.scale` / `service.upgrade` /
`service.setAccess`. `approve` = require a
human: the action returns `approval_required` — the hint prints to **stderr** and the command
**exits 2** (CLI ≥ 0.0.37; distinct from exit 1 = error, so treat exit 2 as "pending, not
failed"); an admin runs `insta approvals approve <id>`, then
you **re-run** it (single-use grant). `project.delete` and `service.remove` are gated by default. `--always` on approve
flips the policy to `allow`.
- `insta approvals list` / `approve <id>` / `deny <id>` — manage pending gates.
- `insta events [--branch] [--limit]` — timeline of resource side-effects (project/branch creates,
deploys + URLs, govern decisions) plus ingested agent events.
- **`insta observe`** — the local credential-audit hook (a PostToolUse hook for Claude Code / Codex,
auto-installed on `project create`/`link`). It scans each tool-use for credential exposure
(AWS / GitHub / Stripe / LLM / DB / JWT / private keys) and appends **redacted fingerprints** (never
raw secrets) to `./.insta/audit.jsonl`. `insta observe report` renders it; `insta observe sync`
uploads findings into the project timeline (idempotent). **(CLI ≥ 0.0.59)** The install
gitignores its own local state (`.insta/observe/`, `.insta/audit.jsonl`) and writes a
shell-neutral `.codex/hooks.json` entry with no machine-specific path, so the hook *entries* in
`.claude/settings.json` / `.codex/hooks.json` are committable while the generated hook files
under `.insta/observe/` are not; never hide `./.insta/project.json` behind a blanket `.insta/`
ignore. Paths git already tracks are reported with a `git rm -r --cached …` hint (an ignore
entry cannot un-track them).
references/branching.md
# Branching — environments with their data
**This is the capability other platforms don't have**: a branch is a disposable, fully isolated
copy of the whole environment — *including the database's data and the bucket's objects* — created
in seconds. Use it as the default unit of ALL work. Never develop on `main`.
**Services are branch-owned, not project-wide.** Each branch has its own service catalog —
`insta services add/list/remove` all default to the **current** branch, and a service added on one
branch does **not** appear on any other branch, including its parent. `insta branch create` **forks**
the parent's current services at creation time (below); after that, the two branches' catalogs
diverge independently — adding, removing, or scaling a service on one has no effect on the other.
**Secrets and compute env are explicit.** The full names-only inventory is
`project → branch → service → secrets` (`insta secrets tree`; a branch's slice is
`insta secrets list`). Provider-minted credentials live under the service that produced them with
canonical names (`DATABASE_URL`, `REDIS_URL`, `MYSQL_URL`, `MONGODB_URL`, `AWS_*`,
`BUCKET_NAME`, …), but they do **not** automatically enter `insta secrets`, `insta run`, or compute
deployments. Bind the provider credentials each compute service needs:
```bash
insta secrets sources --branch feat-x
insta secrets bind DATABASE_URL postgres/db --to compute/app --branch feat-x
insta secrets bindings --target compute/app --branch feat-x
```
For direct access to a branch's DB from outside compute (psql, migrations, local tools):
`insta db url --branch feat-x` prints that branch's connection string; `insta db connect --branch
feat-x` opens psql on it. Before any dump or restore, match the client major to the branch's
`pg_version` (`insta services list --json --branch feat-x`; see [operate.md](operate.md)).
`insta secrets set <NAME> --service compute/app` scopes a **user-defined** secret to that compute
service. It is separate from provider credential binding (`insta secrets bind`). Removing a service
deletes secrets and bindings scoped to it; unbound and project-wide secrets are untouched.
## What `insta branch create <name>` actually clones
| Resource | Mechanism | What the clone contains |
| --- | --- | --- |
| postgres (each) | copy-on-write DB branch | **the parent's data**, isolated — writes never touch the parent |
| storage (each) | copy-on-write bucket fork | **the parent's objects**, isolated |
| compute (each group) | a fresh isolated app + URL per group | **infrastructure only — no code running yet (cloud)** |
| user secrets + compute credential bindings | parent's branch-scoped `secrets set` values and `secrets bind` rules | copied to the new branch with service ids remapped |
Two consequences to internalize:
- **Data clones; code re-materializes.** On the cloud, the clone's compute is an empty app until
you `insta deploy --branch <name>` (insta-oss auto-redeploys the parent's image). Deploy is part
of the branch loop, not an afterthought.
- A legacy project whose root bucket predates snapshots keeps one **shared** bucket — no storage
isolation. `insta manifest` shows what a branch really has.
**Limits:** ≤10 branches per project (hard). `branch create` does **NOT** switch you; compute
scales to zero when idle on every branch — `main` included (a cost lever; compute capacity stays fixed).
## The branch loop (one unit of work)
```bash
insta branch create feat-x [--from <parent>] # isolated env, parent's data
insta branch switch feat-x # per-directory current branch
insta secrets bindings --target compute/app # confirm inherited compute credential bindings
insta secrets # writes user-defined secrets, if any
insta deploy . --port 8080 # put the code on feat-x's compute
# → test against the printed URL (public; verify per deploy.md), iterate freely —
# nothing you do here (schema, data, deploys) can touch main
```
## Parallel agents: 1 task ↔ 1 git worktree ↔ 1 insta branch
Per-branch isolation makes parallel agent development the natural mode. Bind each worktree to a
branch **before** dispatching:
```bash
git worktree add -b feat-x ../proj-feat-x main # isolated CODE copy
cd ../proj-feat-x
insta branch create feat-x && insta branch switch feat-x
npm install # fresh worktrees have NO node_modules — first build fails without this
```
Dispatch one subagent per worktree with a brief like: *"Work only in <dir>. Your environment is
insta branch feat-x (already linked): build, `insta deploy . --port <n>`, capture the printed URL,
verify it serves, then open a PR. Don't touch other branches or switch this directory's link."*
Each agent has its own DB/bucket/compute/URLs — zero collision. In a **shared** checkout, never
`branch switch`; pass `--branch <name>` explicitly instead (switch races the other agents).
## Promotion: merge → migrate → redeploy → validate
**Databases are never merged** — diverged Postgres can't be 3-way merged. Code merges in git;
schema travels as migration **files**:
1. Merge the branch's code in git (parallel-feature conflicts are usually additive — combine).
2. Verify the merged code builds locally *before* the slow deploy.
3. `insta branch switch main` → `insta deploy` the merged code → run the new migration files
against **main's** DB with `insta compute exec app -- <migrate-cmd>` (the bound credentials are
already in the compute env; never gate startup on migrations — see deploy.md).
4. **Validate on main's URL** — promotion isn't done until the live result checks out.
5. `insta branch delete feat-x` — tear down the branch env (may hit a `branch.delete` gate).
**Discipline that makes this work:** every schema change is a file under `migrations/` — it must
replay on a branch DB and again on main. Ad-hoc `psql` schema edits on a branch are lost at
promotion.
## Promote a service to main
Code promotion above doesn't create services — if `feat-x` added one `main` never had (a new
compute group, a storage bucket for a feature about to ship), bring it over **structurally** first:
```bash
insta branch merge feat-x --into main # or: insta branch switch main && insta branch merge feat-x
```
This creates, on `main`, every service `feat-x` has that `main` doesn't — **fresh and empty; no data
is copied** (same rule as code promotion: databases are never merged). Services `main` already has
are skipped (reason `exists` / `cap` / `secret-collision`, printed per service). It's additive only —
nothing on `main` is ever deleted, and re-running it is a no-op for services already merged. Deploy
and seed the newly-created service on `main` same as any other.
## Branch data is disposable by design
Treat branch DB/bucket contents as scratch: experiment, seed, corrupt, measure — then delete the
branch. If an experiment produced data worth keeping, extract it explicitly (dump/script) before
`branch delete`; nothing merges back automatically.
references/deploy.md
# Deploy
Ship code to a branch's compute — image or source — and verify it actually serves.
## Two modes (pick exactly one)
```bash
insta deploy --image <registry/img> --port <n> # prebuilt image — ALWAYS pass --port
insta deploy <dir> --port <n> # source dir — REQUIRES a Dockerfile
# both: [--branch <b>] targets another branch · [--group <g>] picks a compute service by name
```
Targets the **current branch's** sole compute service by default; the URL prints on success.
Before source deploys, run `insta build <dir> --port <n>`. It is local/offline and catches the
common failures before the remote build: missing Dockerfile/start command, wrong or undetected port,
unexpected `.env.example` keys, and an oversized Docker context. Read the verdict literally: only
`deployable` means `insta deploy <dir>` will build it — a dir with no Dockerfile that nixpacks
detects stops at `needs-attention` (⚠ Dockerfile check), because this path needs the dir's own
Dockerfile (CLI ≥ 0.0.48). `--explain` shows the Dockerfile — yours, or the nixpacks one **for
inspection only** (not standalone; do not save it as `Dockerfile`); use `--json` when an agent needs
structured output.
Never run a bare `insta deploy <dir>` and assume the port: without `--port` older CLIs default
to 8080 regardless of the Dockerfile (boots "fine", every request refused — see below). Newer
CLIs default from the Dockerfile's `EXPOSE` and print what they picked — read that line and
confirm it matches the server's listen port.
## How source mode builds (what actually happens)
1. The dir must contain a `Dockerfile`. There is **no nixpacks/buildpack lane on this path** — the CLI exits 1 without one. Dockerfile-less options: add one from the templates below (run `insta build <dir>` first: it reports the detected install/start commands to base it on, and only a dir with its own Dockerfile verdicts `deployable`), use `--image`, or connect the repo on GitHub — that server-side lane builds Dockerfile-less repos with nixpacks. Do **not** save the nixpacks Dockerfile that `insta build --explain` prints as your `Dockerfile`: it `COPY`s `.nixpacks/` support files the dir does not have.
2. Needs the `fly` CLI locally (auto-installed via Homebrew on macOS) but **NO Fly account/login** —
the platform mints a **short-lived, app-scoped deploy token** (this mint is govern-gated: it can
return `approval_required` *before* any build runs).
3. The build runs on **remote builders** (no local Docker); the image is pushed and **pinned by
digest** (tags race the registry), then deployed like any image.
4. insta-oss: source mode is not implemented yet — use `--image`.
## `--port` — the #1 deploy mistake
**`--port` must equal the port the app LISTENS on inside the container** (`EXPOSE` / server bind).
A mismatch boots "successfully" but every request fails (`instance refused connection`). Bind to
`0.0.0.0`, never `127.0.0.1`. On insta-oss it's also the host port for direct deploys; branch
clones keep the listen port and shift the **host** mapping +1000.
## Secrets at runtime
Compute env is explicit. At deploy, the platform injects:
- `PORT`
- user-defined secrets visible to that compute service (`insta secrets set`, project/branch or
compute-scoped)
- provider credentials you explicitly bound with `insta secrets bind`
Provider-minted credentials are **not** injected just because the project has a postgres, redis,
mysql, mongodb, or storage service. Bind each credential the app needs, then deploy/redeploy:
```bash
insta secrets sources
insta secrets bind DATABASE_URL postgres/db --to compute/app
insta secrets bind REDIS_URL redis/cache --source-name REDIS_URL --to compute/app
insta deploy . --group app --port 8080
```
If the source has a single credential (`postgres`), `--source-name` is optional. Sources with several
credential names (`storage`, `redis`, `mysql`, `mongodb`) need `--source-name`. Production code reads
`process.env`; **never bake `./.env` into the image** (it's local-dev/user-secrets only). Changing a
secret or binding takes effect on the **next deploy**, or on **`insta compute restart`** (CLI ≥
0.0.51) for a service already running — no hot reload in either case: the machine takes a new config
and restarts on it, in place. Whether an *idle* machine is woken to do so depends on the compute
provider; see [operate.md](operate.md) before treating a restart as proof the app came back.
Provider credential **values** stay out of the general bundle (`insta secrets` / `insta run` carry
only user-defined secrets). The one direct read is the postgres DSN — `insta db url` /
`insta db connect` (gated `secrets.read`) — for psql, migrations, and tools outside compute; pick
client tools of the server's Postgres major first (`pg_version` on `insta services list --json`; a row
without one falls back to the exact-version read in [operate.md](operate.md)).
Everything else runs where the credentials are bound: the deployed app itself, or a one-shot
`insta compute exec app -- <cmd>` (≤180s, no stdin) — migrations run either way (never as a
startup gate; see the gotchas below).
## Verify before reporting (non-negotiable)
The deploy command exiting ≠ the app serving. After every deploy:
```bash
curl -s -o /dev/null -w '%{http_code}' <printed-url> # poll ~every 3s, up to ~60s
```
Scale-to-zero branches (the default) cold-start on the first request — allow a slow first hit; always-on services (`insta compute always-on on`) skip this. `200` (or the
app's expected status) → report deployed **with the URL**. Anything else → triage per
[operate.md](operate.md); never claim success you didn't observe.
## Deploy gotchas (each has burned real deploys)
- **Never gate container startup on migrations.** `CMD migrate && server` + a hung migration =
a "successful" deploy that serves nothing, with empty logs. Run migrations non-blocking:
`timeout 30 <migrate> || echo skipped; <start-server>`.
- **Cold start ≠ down.** Non-default branches suspend when idle; first request wakes them.
- **Redeploy replaces.** Compute is stateless — anything written to the container filesystem is
gone on the next deploy. State belongs in the branch's postgres/storage.
## Custom domains (bring your own)
```bash
insta compute set-domain app.example.com [--branch --group] # prints the DNS records to add
insta compute check-domain app.example.com # status once DNS propagates
```
Cert + routing are handled for you; the DNS records live in **your** registrar (CNAME for a
subdomain, A/AAAA for an apex, + a validation CNAME).
## Dockerfile templates → use the framework recipes
**Before hand-writing a Dockerfile, copy the recipe for your framework: [frameworks.md](frameworks.md).**
Next.js, Node/Express, Vite/SPA, and FastAPI each have a paste-and-deploy recipe with the four
first-deploy traps already solved (bind `::` not IPv4-only; `EXPOSE` == listen port so `--port`
auto-derives; `PORT` env matches; multi-stage build). Skipping this is why a first deploy boots
"fine" yet refuses every request. Full-stack = one container/one port (backend serves the built
frontend); separate SPA = its own tiny static-server compute service.
references/frameworks.md
# Framework deploy recipes
Copy the recipe for the app's framework **before writing a Dockerfile from scratch** — each one
has the four first-deploy traps already solved, so `insta deploy .` works on the first try.
## The four traps (why first deploys fail)
Every recipe below encodes these. If you hand-write a Dockerfile, get all four right:
1. **Bind `::` (dual-stack), never `0.0.0.0`-only or `127.0.0.1`.** InstaCloud's router and private
network are **IPv6**. An app listening only on IPv4 boots "successfully" and then every request
404s/502s forever. Node's `server.listen(port, '::')`. Next's standalone server does
`server.listen(port, process.env.HOSTNAME || '0.0.0.0')`, and `0.0.0.0` binds **IPv4-only** —
so set **`HOSTNAME=::`** (verified: binds dual-stack). `0.0.0.0` happens to work on Fly's
IPv4-reachable proxy but fails on IPv6-only networks like Railway — `::` is safe on both.
2. **`EXPOSE <port>` in the Dockerfile.** `insta deploy` derives `--port` from the last `EXPOSE`;
without it the service wires to 8080 and refuses every request. Keep `EXPOSE` == the listen port.
3. **`PORT` env == the exposed port.** Read `process.env.PORT` and default it to the same number you
`EXPOSE`. (The platform injects `PORT`; a mismatch is the classic 502.)
4. **Build only what runs.** Multi-stage: build in one layer, copy just the runtime output into a
slim final image. Keeps images small and start fast.
Credentials arrive via injected env only after they are visible to the compute service: user config
from `insta secrets set`, plus provider credentials you explicitly bind with `insta secrets bind`
(`DATABASE_URL`, `BUCKET_NAME`, the `AWS_*` S3 bundle, `REDIS_URL`, `MYSQL_URL`, `MONGODB_URL`, …).
Never bake them into the image.
## Next.js (App Router or Pages) — the common case
`next.config.mjs` **must** set standalone output:
```js
/** @type {import('next').NextConfig} */
export default { output: 'standalone' }
```
`Dockerfile`:
```dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=::
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
```
Then: `insta deploy .` (port auto-derives from `EXPOSE 3000`). Route handlers read `process.env`
for `DATABASE_URL` / the S3 bundle. Pool Postgres at module scope; set `idleTimeoutMillis` under
the database's scale-to-zero suspend window so an idle-suspended DB doesn't leave a dead socket.
## Node/Express (API or full-stack, backend serves the built SPA)
```js
// bind '::' — dual-stack; PORT matches EXPOSE
app.listen(process.env.PORT || 3000, '::', () => console.log('up'))
```
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY . .
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
```
## Vite / static SPA (served by a tiny Node static server)
Build to `dist/`, serve it with a dual-stack static server so client routes fall back to
`index.html`. Simplest is a 15-line Express static server (bind `::`, `EXPOSE 3000`) using the
Node recipe above with `app.use(express.static('dist'))` + a `* → dist/index.html` fallback.
Deploy it as its own compute service; point it at the API via a build-time env var.
## Python / FastAPI (uvicorn)
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8000
EXPOSE 8000
# --host :: binds dual-stack (IPv6 + mapped IPv4)
CMD ["sh","-c","uvicorn main:app --host :: --port ${PORT}"]
```
## After any deploy — verify (non-negotiable)
`curl` the printed URL's health path until it's 200 (cold start takes a few seconds). A 404/502
that never clears almost always means trap #1 (bound IPv4-only) or #3 (PORT≠EXPOSE) — check
`insta logs compute`, which prints the platform's "instance refused connection" hint.
references/governance.md
# Governance & audit
**InstaCloud's gates are enforcement, not etiquette.** On most platforms, agent safety is a
convention the agent is asked to follow; here the control plane refuses the action until a human
approves — an agent that ignores its instructions still can't get past the gate. Work *with* this
system; never around it.
## The gates
Every sensitive action passes a per-project policy check at the credential boundary:
| Action | Default | Guards |
| --- | --- | --- |
| `project.delete` | **approve** | destroying every resource |
| `secrets.read` | allow | plaintext user-secret reads (`insta secrets` / `insta run`), the postgres DSN (`insta db url` / `insta db connect`), and names-only binding/source views; also gates `compute exec`, paired with `deploy` |
| `secrets.write` | allow | user-secret changes and provider credential bind/unbind |
| `deploy` | allow | code reaching compute (and the build-token mint); also gates `compute restart` (which lands configuration through the same path) and `compute exec`, the latter paired with `secrets.read` |
| `branch.delete` | allow | tearing down an environment |
| `service.remove` | **approve** | deleting a service; also gates compute volume delete |
| `service.add/scale/upgrade` | allow | resource mutations (scale/upgrade: paid plans) |
| `storage.read` | allow | listing a bucket, downloading, previewing |
| `storage.write` | allow | uploading an object |
| `storage.delete` | allow | removing objects, one or in a batch |
Decisions: `allow` (proceed) · `deny` (hard no) · `approve` (human in the loop).
`insta compute exec` is the one command gated on **two** actions at once (`deploy` **and**
`secrets.read`) — a `deny` on either is a 403, and an `approve` on either needs its own relay before
the command proceeds. Grants are **single-use and consumed per-gate**, not per-command: with both
actions set to `approve`, a full walkthrough takes **three approvals**, not two. Attempt 1 202s on
`deploy`. Once that's granted, attempt 2 consumes it, passes `deploy`, and 202s on `secrets.read`
(so `deploy`'s grant is already spent again). Once `secrets.read` is granted, attempt 3 needs
`deploy` approved a *second* time before `secrets.read`'s own already-granted grant finally gets
consumed and the command runs. And since an approval records only the action name, not which
command triggered it, the request an admin sees just says `deploy` — nothing distinguishes an
`exec`-triggered approval from a real `insta deploy`.
```bash
insta policy get --json
insta policy set <action> <decision> # admin decision — propose it, don't assume it
```
## The approval flow (relay procedure — CRITICAL)
A gated action returns **"approval required" + an approval id** (HTTP 202; the action did NOT run):
1. **Relay to the human immediately and verbatim**: the exact line, e.g.
`insta approvals approve 7c3c9b68-… ` (`--always` also flips the policy to allow permanently).
Don't summarize it away, don't retry in a loop, don't report failure without surfacing it.
2. Only an **admin** can approve (`insta approvals list --status pending` shows what's waiting).
3. Grants are **single-use**: after approval, **re-run the original command**. The next occurrence
prompts again unless the policy was set to allow.
4. `deny` policy = a hard no: report it and stop. Working around a gate (editing state, bypassing
the CLI) is never acceptable — the gate is the product's safety model.
## The audit timeline
```bash
insta events [--branch <b>] [--limit <n>] [--json]
```
One per-project timeline containing: resource side-effects (creates, deploys + URLs, deletes),
every govern decision (pending/approved/denied, policy changes), and ingested agent findings.
Use it to answer "what happened to this project and who allowed it" — e.g. after any incident,
before deleting anything, or when a human asks what an agent did.
## The observe hook (credential audit for YOUR tool calls)
Auto-installed on `project create`/`link` (PostToolUse hook for Claude Code / Codex):
- Scans each tool call for credential exposure — AWS / GitHub / Stripe / LLM / DB URLs / JWTs /
private keys — and appends **redacted fingerprints** (never raw secrets) to `./.insta/audit.jsonl`.
- `insta observe report [--json]` — review locally. `insta observe sync` — upload findings into
the project timeline (idempotent, deduped).
- Agent etiquette on top of the hook: treat `./.env` as the only credential source; never print
secret values into chat, logs, code, or commits; if the report shows a leak finding, surface it
to the human rather than burying it.
## Patterns for agents
- **Before destructive work** (`project delete`, `branch delete` of someone else's branch): check
`insta events` for recent activity and say what will be destroyed when relaying the approval.
- **Batch your gates:** if a workflow will hit the same gate repeatedly (e.g. many deploys under
`deploy: approve`), tell the human once and suggest `approve --always` or a policy change,
instead of interrupting N times.
- **After approval, verify:** the grant being consumed shows up in `insta events` — confirm the
re-run actually happened before reporting the task complete.
references/mcp.md
# The remote MCP server (`insta-cloud`)
InstaCloud's control plane is also exposed as a **remote MCP server** — Streamable HTTP at
`https://mcp.instacloud.com/mcp` — so MCP-capable agents (Claude Code, Claude.ai / ChatGPT
connectors, Cursor) can drive projects with native tool calls instead of shelling out to the CLI.
It is a stateless bridge over the same platform API the CLI uses: same auth, same governance
gates, same audit trail.
## When to use which
**The skill + CLI is the default — use the MCP tools only when the CLI can't be invoked**:
hosted agents with no shell (Claude.ai / ChatGPT connectors), or a machine where the CLI isn't
installed and can't be. When you do have a shell, prefer the CLI even if MCP tools are also
connected — it carries linked-repo context and covers everything except a few MCP-only
read-only diagnostics (`insta_runtime_health`, `insta_operations`, and `insta_db_stats`'s
`insight`/`activity`/`query-stats` kinds — its `metrics` kind is `insta db stats`; see the
mapping table below), which are fine to call from any client. **The CLI is the only path** for
the things a remote server cannot or must not do:
| Capability | Why CLI-only |
|---|---|
| `insta login` / auth / API-token CRUD | credential minting is deliberately not a remote tool |
| `insta secrets` (pull values → `.env`) / `insta run` | secret **values** never flow out of MCP — names only, values in |
| `insta db url` / `insta db connect` (postgres DSN) | same rule — the DSN is a value read, so it only exists on the CLI |
| `insta deploy <dir>` (source builds) | needs a local build context; `insta_deploy` takes prebuilt image URLs only |
| `insta observe` hook / `insta setup` | local-machine operations |
| `insta db limits` (database machine spec) | not yet exposed as an MCP tool |
## Connecting
`insta setup agent` registers the server with Claude Code automatically (user scope). The default
is **OAuth — no credential is written**: registration is just
```bash
claude mcp add --transport http --scope user insta-cloud https://mcp.instacloud.com/mcp
```
and on first `/mcp` use Claude discovers the platform's authorization server (RFC 9728 → Better
Auth MCP plugin, dynamic client registration) and runs the browser flow — managed, expiring,
revocable tokens, nothing static on disk.
Setup also writes an OAuth (URL-only) entry into the config of **every other detected
MCP-capable agent** — Cursor, OpenAI Codex, OpenCode, GitHub Copilot, Factory Droid — and
`insta mcp install --agent <slug>` targets one explicitly. Merges never clobber existing config
entries.
**Headless machines / CI** (no browser): `insta setup agent --mcp-token` instead mints a durable
`insta_` API token named `mcp-<hostname>` (needs `insta login` first) and registers Claude Code
with an `Authorization: Bearer` header. Manual setup for any other client works the same way:
OAuth if the client supports MCP OAuth discovery, else a Bearer header with any `insta_` API
token.
## Environments
The URL above is production. The MCP host and its **registration name** are resolved from the
CLI's current environment, so the two can never drift apart:
| Environment | MCP server | Registers as |
|---|---|---|
| `prod` (default) | `https://mcp.instacloud.com/mcp` | `insta-cloud` |
| `staging` | `https://mcp.staging.instacloud.com/mcp` | `insta-cloud-staging` |
The distinct names matter: registration is idempotent by name, so a shared name would leave a
staging install silently pointed at the prod server. Because the names differ, **both can be
registered on one machine at once** — check which you're talking to with `insta env`.
`insta setup agent --env staging` (or `curl -fsSL agents.staging.instacloud.com | sh`) switches
the environment and registers staging's server in one step (CLI ≥ 0.0.38 — bare `setup agent`
always targets prod, so a bare re-run after `env use staging` would switch the machine back).
`INSTA_MCP_URL` still overrides outright, for a self-hosted or tunnelled server.
New/renamed tools need a **fresh agent session** to appear — reconnecting an existing session
won't pick them up.
## Tool ↔ CLI mapping
Naming is `insta_<noun>_<verb>`; every tool takes **explicit `projectId` / `branch` args** — the
server is stateless, there is no "current project" like `./.insta/project.json`. Get the
`projectId` from `insta_project_list` (or `.insta/project.json` if you're in a linked repo).
| CLI | MCP tool |
|---|---|
| `insta status` (am I connected?) | `insta_whoami` |
| `insta org list` / `create` | `insta_org_list` / `insta_org_create` |
| `insta project list/create/delete` | `insta_project_list` / `insta_project_create` / `insta_project_get` / `insta_project_delete` |
| region discovery | `insta_regions` |
| `insta services add/list/remove/rename` [`--branch`] | `insta_service_add` / `insta_service_list` / `insta_service_remove` / `insta_service_rename` (all take `branch?`; add takes `public?` for storage) |
| services public/private toggle | `insta_service_access` |
| `insta services scale/upgrade` | `insta_service_scale` / `insta_service_upgrade` |
| `insta compute start\|stop\|suspend\|restart` / `status` | `insta_compute_control` / `insta_compute_status` — `restart` needs a deployed insta-mcp carrying it; older servers reject the verb at schema validation |
| `insta compute exec [service] -- <command>` | `insta_compute_exec` (`name?`/`branch?`/`command`/`timeoutSec?`) |
| `insta compute limits/always-on/volume` | `insta_compute_limits` / `insta_compute_always_on` / `insta_volume` (read/grow: compute + managed fly DBs; remove: compute only, destroys the disk and its data) |
| `insta compute set-domain/check-domain/remove-domain` | `insta_domain_set` / `insta_domain_check` / `insta_domain_remove` |
| `insta branch create/list/merge/delete` | `insta_branch_create` / `insta_branch_list` / `insta_branch_merge` / `insta_branch_delete` |
| `insta manifest` | `insta_manifest` (env view — **no secret values**) |
| `insta secrets list/set/unset` | `insta_secrets_list` (names only) / `insta_secrets_set` / `insta_secrets_unset` |
| `insta secrets sources/bindings/bind/unbind` | `insta_secret_sources` / `insta_secret_bindings` / `insta_secret_bind` / `insta_secret_unbind` (provider credential binding; names only, no secret values) |
| `insta deploy --image <url>` | `insta_deploy` (image-only) |
| `insta metrics/logs/events` | `insta_metrics` / `insta_logs` / `insta_deploy_events` / `insta_events` |
| runtime health / db provider operations (no CLI equivalent) | `insta_runtime_health` / `insta_operations` (database provider operations, not a general operations feed; for watching a postgres branch or restore settle) |
| `insta db stats` (`metrics` kind; `insight`/`activity`/`query-stats` are MCP-only) | `insta_db_stats` (read-only; `kind` = metrics, insight, activity, query-stats) |
| `insta usage` / `billing` | `insta_usage` / `insta_org_usage` (org-level, optional `from`/`to`) / `insta_billing_summary` / `insta_billing_overview` (org-level, current cycle only) |
| `insta billing upgrade/portal` | `insta_billing_checkout` / `insta_billing_portal` — return a Stripe **URL for the human**; relay it, never claim payment happened |
| `insta govern …` (policy/approvals) | `insta_policy_get` / `insta_policy_set` (admin-only; change policy only on explicit human request) / `insta_approvals_list` / `insta_approvals_approve` / `insta_approvals_deny` |
| storage browse/download/delete | `insta_storage_list` / `insta_storage_download_url` / `insta_storage_delete` (no upload yet) |
| `insta template list/info/deploy` | `insta_template_search` / `insta_template_get` / `insta_template_deploy` / `insta_template_deployment_status` |
| `insta feedback` | `insta_feedback` (same fields; pass `projectId`/`branch` explicitly — see [cli-reference.md → Feedback](../cli-reference.md#feedback)) |
## Behavior that carries over from the CLI
- **Governance is identical.** Gated tools return `approval_required` + an `approvalId` instead of
an error — run the same approval relay you'd run for the CLI (tell the human, wait, retry).
Never treat `approval_required` as failure.
- **Custom-domain results include DNS records** for the developer's own registrar — relay them
verbatim, like the CLI's printed table.
- Paid-tier gates (`scale`, `upgrade`) and branch caps (≤10) are enforced by the platform and
surface as structured errors, same as the CLI.
references/operate.md
# Operate — status, triage, and recovery
## Reading the environment
```bash
insta status --json # target api · login · linked project · current branch
insta manifest --json # every branch's db/storage/compute + URLs — the ground truth
insta services list --json # what the project has (postgres rows: pg_version = Postgres major)
insta events --limit 50 # what happened (resources + govern + agent findings)
```
Before any `pg_dump` / `pg_restore` / `psql` against a postgres service, read that service's Postgres
major first: `pg_version` on its `services list --json` row (one per service — a branch with several
postgres services has one each), or `ref.pgVersion` on the manifest's database resources (root and
branch rows alike; the `pg 16` badge on the printed lines needs CLI ≥ 0.0.57). Use client tools of
that same major — a dump taken by a newer client (17 against a 16 server) emits statements the
server rejects, and a default restore keeps going past those errors and leaves the target partially
loaded (`pg_restore` and `psql -f` only roll back as a unit under `--single-transaction`, psql also
with `-v ON_ERROR_STOP=1`). Neither read wakes a suspended instance. Rows older than the field were
backfilled from the image every instance was born from, so if a restore still fails on version
grounds against an old instance, confirm with the exact version. A legacy row that never recorded a
major shows `pg_version: null` and no `ref.pgVersion`; a platform that predates the field sends no
`pg_version` key at all (and no `ref.pgVersion` on any row). For the exact version either way, on
the same branch and service as the DSN (`--group <g>` when the branch has several postgres services):
`psql "$(insta db url --branch <b> [--group <g>])" -c 'show server_version'` answers in one step (it
wakes a suspended instance, like any connection); `insta db stats --json --branch <b> [--group <g>]`
reports it as `serverVersion` but never wakes one, so the field is present only while the instance
is running.
`manifest` is the first stop whenever reality seems to disagree with expectations — it shows what
each branch *actually* has (including a legacy shared bucket, or a compute group that was never
deployed).
## Metrics & logs
```bash
insta metrics compute [group] [--branch --from --to --step --json]
insta logs compute [group] [--branch --limit --region --instance --json]
insta logs compute [group] --since 2h # time window (--from/--to also accepted) — pages ~7 days of history
insta metrics redis|mysql|mongodb [group] # managed DBs are Fly apps: same full metrics/logs
insta logs redis|mysql|mongodb [group] [--deploy]
insta metrics db · insta logs db # postgres: provider-limited — returns a note, not series
```
insta-oss: metrics/logs return a clear "cloud-only / coming" 501 today — use `docker logs`/`docker
stats` on the branch's containers directly if you must, and don't retry the CLI command.
## Idle modes & what an app costs
**Billing is always by actual app usage** — vCPU·min burned, GB·min of RAM resident, storage,
egress — never by machine size × hours. The idle mode only changes what "idle" consumes:
- **Scale-to-zero (default)**: idle machines suspend and auto-wake on the next request. An idle
service costs **nearly nothing**; the trade is a cold start (typically a few seconds) on the
first request after idling.
- **Always-on (opt-in, all plans)**: machines never suspend, so there are **no cold starts** — but
the idle app keeps its RAM resident (plus a trickle of vCPU), and that real usage bills
continuously (roughly $1–2.50/month for an idle minimum-spec app, mostly RAM).
Flip it any time — it is a latency/cost dial, not a plan feature:
- `insta services add compute <name> --always-on` — create pinned-warm.
- `insta compute always-on on|off [service]` — toggle a live service.
- `insta db always-on on|off [--group <g>]` — the same dial for a postgres service:
`off` (default) suspends the idle instance and cold-starts the first connection after idle;
`on` keeps it warm.
## Resource ceilings (limits)
A service's size is **not** a price — billing is actual usage either way. It is a **ceiling**: the
most the app may burn, i.e. its blast radius. So it moves in both directions, and lowering one
costs the customer nothing.
```bash
insta compute limits # ceiling 4 vCPU / 4 GB (plan max 4 vCPU / 4 GB on free; a new service is born AT its plan cap)
insta compute limits --memory 1gb # set it — cpu derives from memory
insta db limits --memory 8Gi --cpu 4 # same dial for postgres
```
- **Memory is the dial.** It is the ceiling that actually bites (hitting it OOM-kills the app);
vCPU only throttles, so it is derived unless `--cpu` is passed for a parallel workload. On
compute, setting always requires `--memory` (`--cpu` is an override, never valid alone); on db,
either flag alone works. Decimal (`mb`/`gb`) and binary (`Mi`/`Gi`) suffixes are both accepted.
- **Plan caps.** A new service is born at its plan's ceiling (free 4 vCPU / 4 GB, pro 8 / 8) and
may move anywhere within it on any plan; raising ABOVE the free cap is the one thing a plan gates,
precisely because usage billing means the size is no longer what you pay for.
- **Compute ceilings snap to the provider's sizes** (vCPU comes from a fixed ladder; memory in
256 MB steps within a per-vCPU band). A request that cannot be honored exactly is REJECTED with
the legal value named — never silently rounded.
- Raising or lowering a compute ceiling **restarts the machine**; a postgres resize restarts the
instance only if it is awake (a suspended one applies the new ceiling on its next wake).
`insta services upgrade` still exists but is the pre-usage-billing control for **compute**: named
specs, up-only. Prefer `limits`. For postgres it is not a fallback at all — an `upgrade` on a
postgres service is rejected outright; resize it with `insta db limits` and grow its disk with
`insta db volume`.
## Compute volumes
Compute persistent `/data` volumes are **not create-time only**:
```bash
insta services add compute app --volume 1Gi # attach at creation
insta compute volume app --size 1Gi # attach later if the service has no volume
insta compute volume app # view size, mount path, and plan cap
insta compute volume app --delete # destroy the disk and all data
```
`--size` on a volumeless service attaches a volume, and it mounts on the **next deploy/redeploy**.
`--size` on an existing volume grows it only; volumes cannot shrink. Deleting is the only way off a
volume and is irreversible. A volume keeps machine count at 1 and changes scale-to-zero from suspend
to stop; those constraints lift after deletion.
## Pausing & resuming compute
To take a service **offline on purpose** — a maintenance window, cost control, or parking a
preview branch — use the lifecycle controls, which are a *persistent* override: a stopped/suspended
service will **not** be re-woken by incoming traffic (unlike scale-to-zero's auto-wake).
- `insta compute stop [service]` — clean shutdown; stays down until `start`.
- `insta compute suspend [service]` — snapshot RAM for a faster resume; stays down until `start`.
- `insta compute start [service]` — bring it back online and re-enable auto-wake.
- `insta compute status [service]` — desired (your intent) vs. live runtime state.
`[service]` is optional when the project has exactly one compute service. These work on all plans and
require no approval. A billing suspension is separate: you can't `start` while an org is billing-
suspended, and a manual `stop` is preserved across a billing pause/resume cycle. A billing
suspension force-stops always-on machines too — pinned-warm does not outlive the org's credit.
## Restarting a compute service
`insta compute restart [service]` (**CLI ≥ 0.0.51**; older builds answer with commander's unknown-command
error) re-runs the image reference the service **already** runs, against a freshly resolved env
bundle. Reach for it in exactly two situations:
1. **Config changed and the running app hasn't picked it up.** `insta secrets set`,
`insta secrets bind` and `insta secrets unbind` all change what the app *would* receive, not what
the running machine holds — env is baked into the machine at deploy time. `restart` is how that
change lands without shipping a new version.
2. **The machine is up but wedged.** A crash-looped or hung process is still `started`, so
`insta compute start` is a no-op on it — it only flips desired state and wakes a machine that is
*down*. `restart` cycles it.
What it is **not**: a new deploy. It asks for no new version and no new spec — the image
*reference*, port and resource ceiling are exactly the ones already recorded on the service.
One qualification, and it is the only way a restart can change what runs: **it does not pin a
digest.** A service recorded against a moving tag (`app:latest`, `nginx:1.27`) gets whatever that
tag resolves to *now* — the same as redeploying that tag would. Source deploys (`insta deploy <dir>`)
record a unique `insta-<timestamp>` label and are unaffected; only `--image` with a moving tag is.
If you are restarting a production app to cycle a wedged machine, that is worth knowing before you
run it.
Rules worth knowing before you call it:
- **The service must be running.** A deliberately stopped or suspended one is refused (400) and
pointed at `insta compute start`, which is also what re-enables auto-wake. A restart would
otherwise silently undo the persistent override `stop` gives you.
- **A service that has never been deployed** is refused the same way `exec` refuses it — deploy an
image first.
- **A machine that was already running is health-gated on the way back up.** If it doesn't answer on
its port, the machines are rolled back — best-effort — to the config they were serving and the
command reports the failure. That verdict is the useful part: a restart that "fails" here is
telling you the app itself is broken, not the platform.
- **An idle machine may not be booted or gated at all — and idle is the default.** What happens to a
scaled-to-zero machine depends on the compute plane behind your deployment — `insta manifest
--json` names it on each compute row (`provider`: `fly` or `microvm`, or the neutral `compute`
when the platform did not report one, in which case assume neither behaviour). On the Fly-backed one it
takes the new config *without waking*, coming up on it at the next request: nothing is
health-checked and no uptime is billed for the restart itself. On the microVM plane the deploy
waits for the service to be running and gates it.
So do not read a fast, green restart of an idle service as proof the app still boots. If that
proof is what you were after, **send it a request** and check the response — that is the one step
that means the same thing on both. (`insta compute always-on on` does *not* substitute: it changes
the idle policy without starting a suspended machine, so it leaves you ungated and pinned warm.)
- **Gated under `deploy`** (unlike `start`/`stop`/`suspend`, which are ungated). Those change whether
the service is running; this changes what it runs — it lands configuration through the same path a
deploy does. So a project with `deploy` set to `deny` refuses it, and one set to `approve` relays it (`insta approvals approve
<id>`; see governance.md). **If you only need to cycle a wedged machine under such a policy, use
`insta compute stop` then `insta compute start`** — that force-stops and relaunches the machine
without going through a deploy. What it will *not* do is pick up new configuration.
- All plans. Refused while the org is billing-suspended — the same door `start` stands behind. Any
machine it wakes bills as ordinary uptime; one left asleep (see above) costs nothing.
- **WebSocket apps keep their concurrency.** The connections-based concurrency `insta deploy
--websocket` sets — and the 512 MB guest floor that rides with it — is recorded on the service, so
a restart re-asserts it, as does any redeploy given no flag. A service deployed before that became
a recorded setting has it recovered from its running machine. You do not need to redeploy a socket
app just to keep it a socket app.
## Running a one-shot command on a compute machine
`insta compute exec [service] -- <command> [args...]` runs a single command on the service's live
machine and returns — **no interactive shell, no stdin**. Use it for a one-off migration, a debug
`ls`/`cat`, or confirming a process is actually up.
- Targets **a single machine** — the first `started` machine, else the first live one. If a service
has been scaled out to multiple machines, `exec` runs on exactly one of them, not all.
- A **scaled-to-zero machine wakes first** — adds a few seconds, and that wake time bills as normal
compute uptime, same as any other request.
- `--timeout <sec>` bounds the run, **1–180s** (default 30); the remote command is killed if it
doesn't finish in time.
- The CLI's **exit code is the remote command's exit code** — safe to check in a script (`&&`,
`$?`). stdout/stderr stream to their own local streams verbatim; each is capped at **1 MiB**, with
a truncation notice on stderr if hit. `--json` returns the raw response instead of split streams.
- Gated on **both** `deploy` and `secrets.read` — a deny on either is a 403; an approve on either
triggers the usual relay (`insta approvals approve <id>`; see governance.md).
- A compute service with no image ever deployed 400s ("this service has no machines yet — deploy an
image first, then retry") — `insta deploy` it, then retry.
`[service]` is optional under the same rule as `start`/`stop`/`status` above.
## Deploy triage (URL not serving after deploy)
Work the list in order — these cover ~all real failures seen so far:
1. **Port mismatch** (most common): `--port` ≠ the port the app listens on. Symptom: deploy
"succeeds", every request refused/000. Fix: redeploy with the app's actual listen port; bind
`0.0.0.0`.
2. **Cold start**: non-default branches suspend when idle — first request can take seconds. Poll
up to ~60s before concluding failure.
3. **Migration-gated startup**: `CMD migrate && server` with a hung migration = nothing listening,
empty logs. Fix the CMD to start the server regardless (see deploy.md).
4. **Read the logs**: `insta logs compute [group] --branch <b> --limit 100` — crash loops, missing
env, bad image arch. A bare read is ONE provider page (~100 lines); when the failure is older
than that, window it: `--since 2h`, or `--from <unix|ISO>` / `--to`.
5. **Stale CLI**: unrecognized command / odd 4xx → `insta upgrade` (or re-run the installer), retry.
6. **Gate, not failure**: a 202 "approval required" is not an error — relay it (governance.md).
## Failure-reporting discipline
Report the exact observed state (HTTP code, log line, gate id) — never an assumed one. A deploy
isn't "done" until the URL served; a promotion isn't "done" until main's URL validated; a teardown
isn't "done" until `manifest`/`events` reflect it.
## Cloud vs insta-oss behavior differences
| Surface | Cloud | insta-oss |
| --- | --- | --- |
| login | required | doesn't exist (localhost trust) |
| usage / billing | real (billing dimensions) | 501 — no billing locally |
| metrics / logs | served (compute full, db limited) | 501 today (docker-stats planned) |
| source deploy (`deploy <dir>`) | ✅ remote build | not yet — use `--image` |
| services add postgres/storage | ✅ (≤5 each) | 501 — one of each, auto-provisioned |
| branch compute | fresh empty app — deploy to it | parent's image auto-redeployed |
| branch app URL | own subdomain | host port +1000 |
references/setup.md
# Setup
From zero to a linked project — CLI install, target selection, auth, project + services.
## Install / upgrade the CLI
```bash
# agent one-liner — CLI + the insta skill for every coding agent + MCP (preferred; any OS/shell,
# Node 18+; self-installs the CLI globally). ALWAYS targets prod (CLI >= 0.0.38 — switches a
# staging-leftover machine back, announced):
npx -y insta@latest setup agent
# staging is its own explicit one-liner (persists the env switch itself):
npx -y insta@latest setup agent --env staging
# no Node? macOS/Linux ONLY — never on native Windows (PowerShell's curl alias + WSL bash shim break it):
curl -fsSL agents.instacloud.com | sh
# staging curl route NOT LIVE YET — until its DNS ships, use the raw URL below:
curl -fsSL agents.staging.instacloud.com | sh
curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh -s -- --agents --staging -y
# CLI only:
curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh # native binary, no Node; macOS/Linux
npm install -g insta # npm alternative · one-shot: npx insta@latest <cmd>
insta setup agent # add the agent skills later (user-global, all agents)
insta upgrade # self-update (auto-update is on by default pre-1.0)
insta autoupdate off # opt out of auto-update
```
Misbehaving or unrecognized command → update first (re-run the installer — it's idempotent — or
`npm update -g insta`), then retry.
## Pick the target
| | InstaCloud (managed) | insta-oss (self-hosted) |
| --- | --- | --- |
| endpoint | platform API (login persists it) | `INSTA_API_URL=http://127.0.0.1:8080` (its default) |
| auth | required (below) | none — localhost trust, builtin `local` user |
| daemon | n/a | `git clone InsForge/insta-oss && npm i && npx tsx src/main.ts` (needs Docker) |
Managed InstaCloud has two environments — `prod` (default) and `staging` — which are **separate
deployments** with separate project lists and separate logins. `insta env` shows which one you're
on; `insta env use <name>` switches (dropping the session, since a token from one is not valid at
the other). Full table in [cli-reference.md](../cli-reference.md#environments).
`insta status --json` shows which target you're on (`env` + `apiUrl`) + login + linked project.
## Auth (cloud only)
- **Agents:** `insta login --email <e> --password <p>` (or `$INSTA_PASSWORD`), or an API token.
- **Humans:** bare `insta login` — opens the console approval page in the browser (any account
type: email, GitHub, Google) and polls until approved; the link + code are also printed. If the
environment can't open a browser, relay the printed link + code to the human **immediately and
verbatim**; never sit on it silently. `--oauth github|google` still opens the named provider
directly.
- **Headless machines (VM, SSH, CI — no browser on THIS machine):** `insta login --device` —
prints a console link + code the human opens **on any other device** and approves; the CLI
polls until logged in (~15 min window). Relay the printed link + code to the human immediately
and verbatim. Don't use `--oauth` here: its loopback callback can never reach this machine.
- There is no login on insta-oss — don't try; `insta login` is a cloud-only command.
## Project
Linking is OPTIONAL (CLI ≥ 0.0.10): the first project-scoped command in an unlinked directory
auto-resolves — one project on the account is picked silently, several give a one-keystroke
picker — and persists to `./.insta/project.json` (commit it: teammates + CI inherit the binding,
and it resolves from any subdirectory, git-style). Resolution order:
`--project/--branch flags > INSTA_PROJECT_ID / INSTA_BRANCH / INSTA_ORG_ID env > link file (walk-up) > auto-resolve`.
Use env for CI/one-offs with no state; use `project link` only to pin a specific project.
```bash
insta project create <name> [--org <id>] # creates an EMPTY project (cloud) and links this dir
insta project link <id> # pin a specific project explicitly (optional)
insta project list --org <id> --json
```
Linking writes `./.insta/project.json` (project + org + current branch, per directory) and
auto-installs the agent skills + the observe credential-audit hook into the repo.
**What to commit vs what the CLI gitignores (CLI ≥ 0.0.59):** commit `./.insta/project.json`
and the hook entries in `.claude/settings.json` / `.codex/hooks.json` (neither contains a
machine-specific path). The CLI adds the machine-local rest to `.gitignore` itself in the same
step that writes it: `.insta/observe/` (the generated hook copy) and `.insta/audit.jsonl` (this
machine's findings), the skill dirs `npx skills add` fills (`.claude/skills/`, `.agents/skills/`;
`.github/skills/` is listed defensively) and `skills-lock.json`. Only the `.insta/*` entries and
`skills-lock.json` are new in 0.0.59; the skill dirs were already ignored. Never ignore `.insta/`
wholesale (that hides the project binding), and don't "clean up" the entries or the ignored
files: a re-link regenerates `.insta/observe/` and the skills, while `.insta/audit.jsonl` is
append-only local findings, so run `insta observe sync` before removing it. An ignore entry cannot
un-track a file that was committed earlier; the CLI reports those with a
`git rm -r --cached …` hint.
**Naming:** use the directory/repo name for the project; things like `api` or `worker` are
*service* names, not project names.
## Services (the project's resources)
A cloud project starts **empty**; add what the app needs (insta-oss auto-provisions one
postgres + one storage at create):
```bash
insta services add postgres db # relational DB (size it with insta db limits)
insta services add storage files # S3-compatible bucket
insta services add compute api # your container; add --volume 1Gi now, or attach later
insta compute volume api --size 1Gi # later attach/grow persistent /data; mounts on next deploy
insta services list --json
```
Compute volumes are **not create-time only**. Use `--volume <Gi>` when adding a compute service if
you already know it needs durable `/data`, or run `insta compute volume <service> --size <Gi>` later
on a volumeless service to attach one. The volume appears on the next deploy/redeploy.
Up to 5 services per type. Provider credentials are minted under the service that owns them with
canonical names (`DATABASE_URL`, `BUCKET_NAME`, `AWS_ACCESS_KEY_ID`, `REDIS_URL`, `MYSQL_URL`,
`MONGODB_URL`, …). They are not exported by `insta secrets` and are not injected into compute until
you bind them to a compute service with `insta secrets bind`. The postgres DSN (only) is also
directly readable — for a local psql, a migration, any tool outside compute — via `insta db url`
(prints it) or `insta db connect` (opens psql); match those client tools to the server's Postgres
major first (`pg_version` on `insta services list --json`, see [operate.md](operate.md)). The other
services' credentials have no direct read and reach code only through binding.
## Ship-from-zero (the whole chain)
```bash
insta status # target + auth + link in one look
insta login … # cloud only, if needed
insta project create myapp
insta services add postgres db && insta services add compute app # cloud; oss has db+storage already
insta secrets bind DATABASE_URL postgres/db --to compute/app
insta deploy . --port 8080 # or --image <ref>; then VERIFY the URL (see operate.md)
```
references/storage.md
# Storage buckets
`insta services add storage <name>` gives the branch a **private, S3-compatible bucket**. There is
no vendor SDK and no InstaCloud storage client — point any S3 library at the bound credentials and
it works. This page is the part that isn't obvious: how bytes actually get in and out, and the
handful of things that bite first.
## What you get
Adding the service mints these credentials under the storage service:
| Variable | Example |
| --- | --- |
| `BUCKET_NAME` | `insta-b5f7e21d-…-f1de1ba6` |
| `AWS_ACCESS_KEY_ID` | `tid_…` |
| `AWS_SECRET_ACCESS_KEY` | `tsec_…` |
| `AWS_ENDPOINT_URL_S3` | `https://t3.storage.dev` |
| `AWS_REGION` | `auto` |
They do **not** automatically appear in every compute service. Bind the names your app needs into
the target compute service, then deploy/redeploy:
```bash
insta secrets bind BUCKET_NAME storage/files --source-name BUCKET_NAME --to compute/app
insta secrets bind AWS_ACCESS_KEY_ID storage/files --source-name AWS_ACCESS_KEY_ID --to compute/app
insta secrets bind AWS_SECRET_ACCESS_KEY storage/files --source-name AWS_SECRET_ACCESS_KEY --to compute/app
insta secrets bind AWS_ENDPOINT_URL_S3 storage/files --source-name AWS_ENDPOINT_URL_S3 --to compute/app
insta secrets bind AWS_REGION storage/files --source-name AWS_REGION --to compute/app
```
Never bake these into an image; after binding, they arrive as runtime env on deploy.
## Writing and reading objects
Nothing InstaCloud-specific. Two rules cover it: **set the endpoint explicitly**, and leave the
region as `auto`.
```js
// Node — @aws-sdk/client-s3
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3 = new S3Client({
endpoint: process.env.AWS_ENDPOINT_URL_S3, // required — without it the SDK talks to real AWS
region: process.env.AWS_REGION, // 'auto'
}) // key/secret come from AWS_* automatically
await s3.send(new PutObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: 'avatars/u1.png',
Body: bytes,
ContentType: 'image/png',
}))
```
```python
# Python — boto3
import boto3, os
s3 = boto3.client('s3', endpoint_url=os.environ['AWS_ENDPOINT_URL_S3'], region_name=os.environ['AWS_REGION'])
s3.put_object(Bucket=os.environ['BUCKET_NAME'], Key='avatars/u1.png', Body=data, ContentType='image/png')
```
The same credentials drive any S3 tool, which is the quickest way to seed or inspect a bucket
(these examples assume an environment where the `AWS_*`/`BUCKET_NAME` values are already
bound/configured — storage credentials have no CLI direct-read the way the postgres DSN does):
```bash
aws s3 ls "s3://$BUCKET_NAME" --recursive --endpoint-url "$AWS_ENDPOINT_URL_S3"
aws s3 cp ./dist "s3://$BUCKET_NAME/dist" --recursive --endpoint-url "$AWS_ENDPOINT_URL_S3"
rclone copy ./dist insta:$BUCKET_NAME/dist # with the same key/secret/endpoint configured
```
## The traps
1. **Forgetting the endpoint.** An S3 client with credentials but no `endpoint` silently talks to
real AWS and fails on a bucket that isn't yours. This is the single most common mistake.
2. **Assuming a bucket is shared across branches — or assuming it never is.** Normally each branch
gets its **own** bucket (CoW-forked from the parent at `insta branch create`) with its **own**
scoped key, so a leaked branch credential cannot reach production data. **The exception is a
legacy project whose root bucket predates snapshots: it keeps one shared bucket, with no storage
isolation at all** — a branch writes straight into production's objects. `insta manifest` shows
what a branch really has, and it is the only way to know which case you are in. Either way, read
`BUCKET_NAME` from env per branch rather than hardcoding a name you saw once.
3. **Expecting a branch's files to be promoted.** They are not. `insta branch merge` creates missing
services on the target **fresh and empty — no data is copied**, the same rule that applies to
databases. Files uploaded while testing on a branch stay there; extract anything worth keeping
before `branch delete`. See [branching.md](branching.md).
4. **Uploading without a `ContentType`.** S3 stores what you send and serves it back. Omit it and the
object comes back as `application/octet-stream`, which makes a browser download it instead of
showing it — so the console's preview, and any `<img src>` you point at a presigned URL, silently
degrade. Always set it.
5. **Expecting to undelete.** There is no object versioning and no recycle bin. `insta storage
delete` and any S3 `DeleteObject` are permanent, and deleting a key that was never there still
reports success — so success is not proof the file existed.
6. **Expecting search.** S3 filters by **key prefix** only; there is no substring match, in the CLI,
the console, or the API. Design keys so the prefix is the thing you will want to filter on
(`avatars/2026/…`, not `2026-avatars-…`).
## Public vs private
Buckets are private by default: reads need the credentials or a presigned URL. Flip a bucket to
anonymous public-read with
```bash
insta services set-access storage <name> public # or private
```
Public is a whole-bucket switch, not per-object. When only *some* files should be reachable, keep the
bucket private and have your own backend hand out a short-lived URL per request — the caller never
sees the credentials, and the link expires:
```js
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({ endpoint: process.env.AWS_ENDPOINT_URL_S3, region: process.env.AWS_REGION })
// Your route does the authorization, then signs. 5 minutes is plenty for a redirect.
export async function fileUrl(key) {
return getSignedUrl(s3, new GetObjectCommand({ Bucket: process.env.BUCKET_NAME, Key: key }), {
expiresIn: 300,
})
}
```
That is the same mechanism the console and `insta storage get` use, so a private bucket is not a
limitation on serving files — only on serving them anonymously and forever.
## Managing a bucket without an S3 client
The platform exposes the objects directly, so the CLI, the console and MCP can all reach them:
```bash
insta storage list # keys, size, last modified (--prefix to filter, --cursor to page)
insta storage get <key> -o ./file # short-lived presigned URL; bytes come straight from the provider
insta storage delete <key> # immediate and irreversible, no prompt
```
The console's storage service detail browses the same objects with preview, download, and single or
bulk delete. Agents get `insta_storage_list` / `insta_storage_download_url` / `insta_storage_delete`
over MCP — note the download tool returns a **URL, not bytes**.
Every path is governed (see [governance.md](governance.md)):
| Action | Covers |
| --- | --- |
| `storage.read` | listing, download, preview |
| `storage.write` | upload |
| `storage.delete` | single and bulk delete |
**Upload is console-only for now.** The browser uploads straight to the provider under a signed
policy that pins the content type and caps the size, so nothing streams through the control plane.
The CLI and MCP have no upload yet — from a script, use an S3 client or `aws s3 cp` as above.
SKILL.md
---
name: insta
description: >
Operate InstaCloud infrastructure with the `insta` CLI: create projects, add
postgres/storage/compute services, deploy apps, create disposable branch
environments (isolated DB + storage + compute per branch), bind service
credentials into compute env, wire user secrets into `.env`, run multiple
agents each in their own branch, handle governance
approvals, check metrics/logs/usage, and promote branches to main. Use this
skill when working in an InstaCloud-managed project (a `.insta/` dir or the
`insta` CLI), when the user mentions InstaCloud or insta, AND when they ask to
deploy an app, need a database/backend/object storage, want preview or
per-agent sandbox environments, want branchable infrastructure, or mention
agent setup or MCP — even if they don't say "InstaCloud" explicitly. Also
covers the insta-cloud remote MCP server (insta_* tools) and the self-hosted
insta-oss runtime (same CLI, local daemon).
allowed-tools: Bash(insta:*), Bash(npx:*), Bash(curl:*), Bash(command:*), Bash(git:*), Bash(npm:*)
---
# InstaCloud
InstaCloud provisions and governs a project's cloud services behind one CLI and one credential
seam. The `insta` CLI talks **only** to the InstaCloud control plane — you never configure a cloud
backend directly. A project can have any number of **services**, added on demand. The common service
types you build directly against are:
- **postgres** — relational DB born at its plan's resource ceiling (move it within the free cap on
any plan with `insta db limits`; above the free cap needs a paid plan). Plain Postgres: connect any driver/ORM directly with the `DATABASE_URL`
you bind into compute env (below) — no vendor SDK or vendor skill. The DB is also publicly
dialable from outside compute: `insta db url` prints the connection string and
`insta db connect` opens a psql session — that's how you (or a human) reach it from a laptop,
a migration script, or any external tool. It scales to zero when
idle, so keep your pool's `idleTimeoutMillis` under the suspend window (see
[frameworks.md](references/frameworks.md)).
- **storage** — S3-compatible object/blob storage. Point any S3 library at the bound `AWS_*` /
`BUCKET_NAME` env — no vendor SDK. Set the endpoint explicitly or the client talks to real AWS;
each branch normally gets its own forked bucket (legacy pre-snapshot projects share one — see
below). See [storage.md](references/storage.md).
- **compute** — your container(s) at a public URL. A project can have several compute services
(e.g. `api`, `worker`).
- **redis/mysql/mongodb** — managed Fly-backed data services. They expose connection env names such
as `REDIS_URL`, `MYSQL_URL`, and `MONGODB_URL`.
**A new project starts empty** — no services are created automatically. Add what you need:
`insta services add postgres <name>`, `insta services add compute <name>`,
`insta services add storage <name>`, `insta services add redis <name>`, etc. A project may have
**multiple services of every type** (up to 5 per type). Provider credentials are scoped to the
service that minted them and use canonical names inside that scope (`DATABASE_URL`, `REDIS_URL`,
`MYSQL_URL`, `MONGODB_URL`, `AWS_ACCESS_KEY_ID`, `BUCKET_NAME`, …). They do **not** automatically
appear in `insta secrets`, `insta run`, or compute env. Bind the credentials a compute service needs,
then deploy — or, if the service is already running, `insta compute restart` (CLI ≥ 0.0.51) to pick
the binding up without deploying a new one. It re-runs the image *reference* already recorded, so a
service on a moving tag (`app:latest`) still gets whatever that tag resolves to now — see
[operate.md](references/operate.md) before using it on production:
```bash
insta secrets sources # what's available to bind (--branch <b> targets another branch)
insta secrets bind DATABASE_URL postgres/db --to compute/app
insta secrets bind REDIS_URL redis/cache --source-name REDIS_URL --to compute/app
insta secrets bind MYSQL_URL mysql/orders --source-name MYSQL_URL --to compute/app
insta secrets bind MONGODB_URL mongodb/catalog --source-name MONGODB_URL --to compute/app
insta deploy . --group app --port 8080
```
Binding is for **compute env** only. To use a credential yourself — run migrations, inspect data,
point a local tool at the DB — read the value directly: `insta db url` (postgres connection
string; `insta db connect` for a psql shell).
Use `insta services rename <type> <name> <new-name>` to rename a service; existing bindings keep
pointing at that service.
## Install & upgrade the CLI
If `command -v insta` finds nothing, install it (never assume it's present):
```bash
curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh # native binary, no Node
npm install -g insta # npm alternative
npx insta@latest <cmd> # one-shot, always newest (slow per call)
```
The CLI is pre-1.0 and ships often. If a command misbehaves or is unrecognized, **update first**:
`insta upgrade` (CLIs that have it; auto-update is on by default pre-1.0 — `insta autoupdate off`
to disable), else re-run the installer (idempotent) or `npm update -g insta`.
## Two targets, one CLI
The same commands drive both. Resolve which one you're on from `insta status` (`api:` line):
- **InstaCloud (managed cloud)** — requires `insta login` (agents: `--email/--password` or an API
token; humans: bare `insta login` opens the console sign-in/approval page in the browser — any
account type; headless machine with a human reachable elsewhere: `--device` prints a link + code
they approve from any other browser).
- **insta-oss (self-hosted local daemon)** — `INSTA_API_URL=http://127.0.0.1:8080` (its default).
**No login exists or is needed** (localhost trust, builtin `local` user); billing/usage/metrics
return clear "cloud-only" errors — don't retry them.
## Tool routing: CLI vs MCP
InstaCloud has two agent-facing operation paths. **This skill + the CLI is the default for
everything** — the CLI covers the full command surface (bar a few MCP-only read-only
diagnostics, listed in mcp.md), carries linked-repo context
(`.insta/project.json`), and is the only path for local machine state: auth (`insta login`),
pulling secret **values** (`insta secrets` / `insta run`, and the postgres DSN via
`insta db url` / `insta db connect`), source-directory deploys (`insta deploy <dir>`), and the
observe hook.
Fall back to the **remote MCP tools** (`insta_*`) only when the CLI can't be invoked: no shell
(hosted agents like Claude.ai / ChatGPT connectors), or the CLI isn't installed and can't be
(the common case is fixable with no CLI on PATH: `npx -y insta@latest setup agent -y`
self-installs it — see self-heal below). Same platform API, same governance gates, same audit
trail — but MCP tools take **explicit `projectId`/`branch` args**: never assume the CLI's linked
context carries over; resolve IDs first (`insta_project_list`) and pass them explicitly. Full
mapping + connection guide: **[mcp.md](references/mcp.md)**.
**Self-heal:** if the insta skill or the `insta_*` MCP tools are expected but missing, run
`insta setup agent -y` (installs the skill + registers MCP for Claude Code and every detected
agent), then tell the user to **restart their coding tool** — a running session never picks up
newly registered MCP servers or tools. One specific agent: `insta mcp install --agent <slug>`.
## Intent-based routing
Route by intent before running preflight ceremony:
**"Ship / deploy this app" (from zero):** don't interrogate state first — run the chain and
announce it: `insta status` (logged in? linked?) → if unauthenticated on cloud, `insta login` → if
unlinked, `insta project create <dir-name>` → `insta services add postgres db` (if the app needs a
DB) + `insta services add compute app` → bind needed service credentials into compute
(`insta secrets sources`, then `insta secrets bind DATABASE_URL postgres/db --to compute/app`) →
`insta deploy . --port <the port the app listens on>` → **verify the printed URL serves** (below).
The app reads `process.env` creds.
**"Set up / onboard / sign up":** cloud → `insta login` (browser sign-in; relay the printed link
if no browser opens) or `--email/--password`; then `insta project create`. Local/oss → nothing to set up beyond the daemon.
**A unit of work on an existing project (feature, fix, experiment, agent task):** one branch per
unit of work — see the core principle below and **[branching.md](references/branching.md)**.
Never develop on `main`.
**Anything else (configure, debug, inspect):** light preflight, then the matching reference below.
## Preflight & context (before mutations)
```bash
command -v insta # installed? (else: Install section)
insta status --json # target api, login, linked project, current branch
```
Skip this ceremony for the ship-from-zero chain above — `status` is its first step already.
**Context rules (multi-agent safety):**
- The link (`./.insta/project.json`) is **per directory** and includes the current branch.
- **Prefer explicit `--branch <name>`** on commands that accept it (`secrets`, `deploy`, `metrics`,
`logs`, `events`, `db url` / `db connect` — a wrong-branch DSN means querying the wrong
database) over `insta branch switch` when acting on a branch you don't own — `switch`
mutates the shared per-directory link and races parallel agents in the same checkout.
- For parallel agents, the rule is **1:1:1 — task ↔ git worktree ↔ insta branch** (each worktree has
its own link, so `switch` is safe there). See [branching.md](references/branching.md).
## Core principle
**One unit of work = one branch = one isolated environment.** `insta branch create <name>`
materializes the **parent branch's** current services onto the new branch — a CoW database branch
(copy of the parent's data), a CoW-forked storage bucket, and a clone of every compute service (own
URL each), created **at branch-create**, so a branch is a complete runnable environment from the
start.
Branches run fully in parallel; nothing one does touches another. **≤10 branches per project (hard
limit).** Don't develop on `main`; don't pile multiple features on one branch.
**Multiple independent features (or agent tasks) at once?** Give each its own branch **and its own
subagent** — isolated DB + storage + compute + URLs mean zero collision. See
**[branching.md](references/branching.md) → Parallel agents**.
## Verify before reporting (deploys)
**Never report a deploy as successful from the command exiting alone.** `insta deploy` prints the
branch URL on success — that means the platform accepted and rolled the machine, not that the app
serves:
1. Poll the printed URL (`curl -s -o /dev/null -w '%{http_code}'`) every ~3s for up to ~60s.
Scale-to-zero branches (the default) cold-start on the first request — allow a slow first hit
(always-on services skip this; see references/operate.md).
2. `200` (or the app's expected status) → deployed; report the URL.
3. Still failing → the ordered triage list in [operate.md](references/operate.md) (port mismatch
and migration-gated startup account for most failures).
4. Report the exact failing state — never claim success you didn't observe.
## Approval relay (CRITICAL — gated actions)
Sensitive actions are gated at the credential boundary (`secrets.read`, `secrets.write`, `deploy`,
`project.delete`, `branch.delete`, `service.add/remove/scale/upgrade`; policy per action:
allow/deny/approve — `project.delete` and `service.remove` require approval by default). When a command returns
**"approval required" with an approval id**:
- **Relay it to the human immediately and verbatim** — the exact line to run:
`insta approvals approve <id>` (add `--always` to also stop future prompts for that action).
Don't summarize it away, don't retry the command, and don't report the task as failed without
surfacing the approval first. Only an **admin** can approve.
- Grants are **single-use**: after approval, **re-run the original command**; the next occurrence
prompts again unless policy was set to allow (`--always` / `insta policy set <action> allow`).
- **Never work around a gate** (e.g. by hand-editing state or bypassing the CLI) — the gate is the
product's safety model. A `deny` policy is a hard no: report it, don't circumvent it.
## Common quick operations
```bash
insta status --json # target, login, link, current branch
insta manifest --json # agent-legible env view: every branch's services + URLs
insta services list --json # what exists on this project
insta run -- <cmd> # run with user-defined secrets injected (NOTHING on disk; --branch <b>)
insta secrets --print # user-defined secrets for the current branch (--branch <b>)
insta secrets sources --json # provider credential sources available to bind
insta secrets bind DATABASE_URL postgres/db --to compute/app
insta secrets bindings --target compute/app --json
insta secrets set NAME value # user config (project-wide; --branch for overrides)
insta build . --port 8080 # local pre-deploy build/readiness check
insta deploy . --port 8080 # build (Dockerfile) + deploy to the current branch
insta deploy --image <ref> --port 8080 # prebuilt image instead
insta compute exec app -- printenv PORT # one-shot command on live compute (no shell/stdin)
insta compute volume app --size 1Gi # attach/grow persistent /data; mounts on next deploy
insta branch create feat && insta branch list --json
insta logs compute --limit 100 --json # runtime logs (--branch <b>; also redis|mysql|mongodb; db is provider-limited)
insta logs compute --since 2h --json # time window (--from/--to too) — a windowless read is ONE page (~100 lines)
insta metrics compute --json # service metrics (also redis|mysql|mongodb)
insta events --limit 50 --json # audit + agent-event timeline
insta usage --json # cloud only (insta billing --json likewise)
insta approvals list --status pending # outstanding gates
```
Use `--json` wherever you parse output.
## Routing
For anything beyond the quick operations, load the reference that matches the intent — one is
usually enough, two at most:
| Intent | Reference | Covers |
| --- | --- | --- |
| Create or connect things ("set up", "new project", "add a database/compute") | [setup.md](references/setup.md) | CLI install/upgrade, cloud vs oss target, auth, project, services, ship-from-zero |
| Ship code or manage releases | [deploy.md](references/deploy.md) · framework recipes: [frameworks.md](references/frameworks.md) | image vs source (remote build), `--port` semantics, explicit service credential binding, secrets at runtime, verify procedure, Dockerfile templates, custom domains |
| Branch environments, parallel agents, promotion ("preview env", "sandbox per task", "merge to main") | [branching.md](references/branching.md) | **the data-forking env model** (what actually clones), branch loop, 1:1:1 worktree pattern + dispatch brief, promotion, migration discipline |
| Approvals, policy, audit, credential scanning | [governance.md](references/governance.md) | gates catalog, the approval relay, events timeline, observe hook, agent audit patterns |
| Check health or debug failures | [operate.md](references/operate.md) | status/manifest triage, ordered deploy-failure list, metrics/logs, cloud-vs-oss differences |
| Command lookup | [cli-reference.md](cli-reference.md) | the full CLI catalog with flags and gates |
| Remote MCP tools ("connect a connector", `insta_*` tools available) | [mcp.md](references/mcp.md) | connecting clients, tool ↔ CLI mapping, what stays CLI-only |
| InstaCloud itself got in your way (bug, stale doc, missing feature, friction) | [cli-reference.md → Feedback](cli-reference.md#feedback) | `insta feedback` / `insta_feedback`: when to file, situation → type mapping |
If a request spans two areas ("deploy and check it's healthy"), load both and answer once.
## Two non-negotiables (wherever you are)
- **Prefer `insta run -- <cmd>`** for user-defined project/branch secrets — the bundle is fetched per
invocation and injected into the child environment only; nothing is written to disk, so nothing can
leak or be committed. Provider-minted service credentials are not in this bundle; bind them to a
compute service with `insta secrets bind`, then deploy (or `insta compute restart` an already-running
service, CLI ≥ 0.0.51 — a binding change never reaches a live machine on its own).
- When a file is genuinely needed, treat `./.env` (from `insta secrets`; auto-gitignored in git
repos) as the **only** file-based source for user-defined secrets — never hardcode or print secret
values. `DATABASE_URL`, `AWS_*` / `BUCKET_NAME`, `REDIS_*`, `MYSQL_*`, and `MONGODB_*` are service
credentials that reach production compute only through explicit `insta secrets bind` rules. For
direct use **outside** compute the sanctioned read is `insta db url` / `insta db connect`
(postgres; gated `secrets.read`) — pipe it (`psql "$(insta db url)"`), never paste the DSN into
files or code. Everything else runs where the credentials are bound (the app itself, or a
one-shot `insta compute exec <svc> -- <cmd>`).
User-set config belongs in `insta secrets set <NAME>` (project-wide) / `--branch` for branch
overrides — never hand-edit `.env` values you want to persist.
- Track **every** schema change as a file under `migrations/` so it replays on a branch DB and again
on `main` after a merge. **InstaCloud never merges databases — only migration files carry schema forward.**
Migrations run where the DB credentials are bound: on the compute service, via
`insta compute exec app -- <migrate-cmd>` (never as a startup gate — see
[deploy.md](references/deploy.md)); or directly, with no compute involved:
`psql "$(insta db url --branch <b>)" -f migrations/<file>.sql` (explicit `--branch` — the bare
form reads the linked branch's DB). Match `psql` / `pg_dump` / `pg_restore` to the server's
Postgres major first — `pg_version` on `insta services list --json --branch <b>` (same branch as
the DSN); if the row has none, read the exact version instead (see
[operate.md](references/operate.md)).
## Governance & audit (this is the platform's core differentiator)
The gate mechanics and the relay procedure are above; the observe credential-audit hook, the events
timeline, and agent audit patterns are in [governance.md](references/governance.md).
**Billing is by actual app usage** (vCPU·min / RAM GB·min actually consumed + storage + egress —
not machine size × hours). Scale-to-zero is the default, so idle services cost nearly nothing;
`always-on` (all plans: `insta compute always-on`, `insta db always-on`, or `--always-on` at
create) trades a small continuous RAM cost for zero cold starts — see
[operate.md](references/operate.md). **The paid levers are the resource CEILING** (`insta compute limits`,
`insta db limits` — per-machine size, see [operate.md](references/operate.md))
**and machine COUNT** (`insta services scale` — horizontal): a new service is born at its plan's
ceiling and free plans may move within the free cap but not above it, and stay at one machine —
beyond either is a 403 — `insta billing upgrade` first; `insta usage` /
`insta billing` show cycle usage and cost. One free org per user. Full flags in
[cli-reference.md](cli-reference.md).
## When InstaCloud itself gets in your way (feedback)
If you hit a hurdle that is **InstaCloud's fault** — a command that violates its documented
contract, skill/doc text that doesn't match reality, a missing capability, confusing UX — report
it with `insta feedback` (or the `insta_feedback` MCP tool), **then continue the user's task with
a workaround**. Never block on the report, and **never file feedback for problems in the app the
user is building** — this channel is only for the InstaCloud toolkit
(`--component cli|mcp|platform|skills|docs`). Full flags and the situation → type mapping:
[cli-reference.md → Feedback](cli-reference.md#feedback).
## Response format
For operational work, report: **what was done** (action + scope: project/branch/service), **the
result** (URLs, IDs, observed status — not assumed), and **what's next** (or that it's complete).
Include command output only where it helps.