# Plinth for agents

Plinth hosts an app end to end: git repo, builds, deploys, Postgres per environment, migrations, cron, domains, logs. You drive it. Your human owner approves risky steps.

API: https://tryplinth.dev

## Install

```sh
curl -fsSL https://tryplinth.dev/_plinth/install.sh | sh        # CLI at ~/.local/bin/plinth
```

MCP config (Claude Code, Codex, Cursor, any MCP client):

```json
{ "mcpServers": { "plinth": { "command": "npx", "args": ["-y", "https://tryplinth.dev/_plinth/cli.tgz", "mcp"],
  "env": { "PLINTH_URL": "https://tryplinth.dev" } } } }
```

## 1. Get an account (owner approves once)

```sh
plinth signup --email owner@example.com --agent "Claude Code" --project my-app
```

Prints an approve URL. Give it to your human. If that email already has a Plinth account, you join it as another agent (your own key, same projects and rules); your human approves while signed in to their console. The command waits, then saves your token to `~/.plinth/config.json`.
Raw HTTP: `POST /v1/signup {"email","agent","project"}` → `approve_url`, `poll_url`. Poll until it returns `token` (once).

## 2. Ship

```sh
plinth link my-app          # adds git remote "plinth" with credentials
git add -A && git commit -m "first"
plinth push main            # main deploys to staging; waits and prints the URL
plinth push feat/x          # any other branch gets its own preview env + database copy
plinth promote              # staging -> production; owner approves
```

## What Plinth runs

A release is built from your commit:

- `package.json` dependencies are installed; a `build` script runs if present.
- Static files: first of `dist/`, `build/`, `out/`, `public/`, `./` that has `index.html`.
- Functions: `api/**/*.js` (also `.mjs`, `.ts`). `api/users/[id].js` serves `/api/users/:id`.
  Export `GET`/`POST`/… or `default`: `(request, { params, sql, env }) => Response | object`.
  `sql(text, params)` queries this environment's Postgres. `process.env.DATABASE_URL` is set too.
- Or a server: `"start"` in `plinth.json` (or `npm start` when there is no static/api). Listen on `process.env.PORT`.
- Migrations: `migrations/*.sql`, applied in name order on every deploy and promote, each in a transaction, after an automatic snapshot.
  Destructive SQL (DROP, TRUNCATE, unscoped DELETE/UPDATE, type changes) reaching production waits for the owner.

Single-page apps work out of the box: unknown paths that ask for HTML get `index.html`. Set `"spa": false` to serve real 404s.

Every build and every running app is its own gVisor sandbox: its own kernel, read-only code at `/app`,
writable `/tmp` only, 256 MB RAM, half a CPU, its own network. Apps can reach the internet and their own
database, nothing else. Write files to the database or an external store, not to disk.

Optional `plinth.json`:

```json
{
  "build": "npm run build",
  "static": "dist",
  "spa": true,
  "functions": "api",
  "crons": [{ "name": "digest", "schedule": "0 9 * * 1", "path": "/api/cron/digest" }]
}
```

Cron calls `POST <path>` on that environment (UTC, 5 fields) with the header `x-plinth-cron-secret`
(also in `process.env.PLINTH_CRON_SECRET`). Put handlers under `/api/cron/`: Plinth refuses every other caller there.
Elsewhere, check `ctx.cron` in your function. A failed run is retried once after 30s; overlapping runs are skipped.
Production jobs run by default. Staging and preview jobs start paused: pass `"enabled": true` or call `cron.resume`.

## Environments

| env | tracks | database | url |
|---|---|---|---|
| preview | its branch | copy of staging at creation | `https://<branch>--<project>.<apps domain>/` |
| staging | main | own, nightly snapshot | `https://staging--<project>.<apps domain>/` |
| production | promote only | own, nightly snapshot, 7 kept | `https://<project>.<apps domain>/` or a custom domain |

Each environment has its own hostname, so apps run at `/` exactly as in local dev. `env.list` returns the real URLs.
Secrets you set with `env.secrets.set` are encrypted at rest and never returned.

## Environment variables

```sh
plinth env.secrets.set --env staging --key STRIPE_SECRET --value sk_test_...
plinth env.secrets.import --env staging --file .env      # many at once, one restart (--replace true removes others)
plinth env.secrets.list --env staging                    # names, when and by whom; values are never shown
plinth env.secrets.unset --env staging --key OLD_KEY
```

Each environment has its own variables, encrypted at rest; setting one restarts that environment. New preview environments start with a copy of staging's. Variables starting with `VITE_`, `NEXT_PUBLIC_`, `PUBLIC_`, `REACT_APP_`, `NUXT_PUBLIC_`, `EXPO_PUBLIC_` or `GATSBY_` are also passed to the build, because frontend tools bake them into the browser bundle: never put secrets in those. Plinth sets `DATABASE_URL`, `PORT`, `NODE_ENV`, `PLINTH_PROJECT`, `PLINTH_ENV` and `PLINTH_CRON_SECRET` for you.

## Debugging

```sh
plinth env.status --env staging             # running? restarts, out-of-memory kills, memory/CPU, last errors, hints
plinth logs.query --env staging --since 1h --stream stderr
plinth requests.query --env staging --min_status 500    # failing requests
plinth http.probe --env staging --path /api/orders     # reproduce one request, see status, headers, body
plinth env.exec --env staging --command "node -e 'console.log(process.env.DATABASE_URL ? 1 : 0)'"
plinth env.restart --env staging
plinth deploy.status --env staging          # releases and the latest build log
```

## Analytics

`plinth analytics.get --env production` returns visitors and page views per day, top pages, referrers and devices. No cookies are used. Free shows today; Pro keeps 90 days.

## Support

Stuck, found a bug, or need a feature? Tell the Plinth team directly:

```sh
plinth support.send --kind bug --title "Short summary" --body "Steps, expected, actual, error text" --project my-app --env staging
plinth support.list          # status and the team's reply
```

Recent errors from the environment are attached automatically. When the team replies, the reply is attached (once) to the result of your next Plinth command or MCP tool call, as `notices`; act on it and tell your human if it matters.

## Webhooks

```sh
plinth webhooks.add --url https://hooks.slack.com/services/... --events '["deploy.failed","approval.requested"]'
plinth webhooks.test
plinth webhooks.list
```

Events: deploy.succeeded, deploy.failed, approval.requested, approval.decided, app.down, app.up, support.created, support.replied, charge.succeeded. JSON body `{event, ts, data, text}`, signed with `x-plinth-signature: sha256=<hex HMAC of the raw body>`. Slack and Discord incoming-webhook URLs and Telegram `sendMessage?chat_id=` URLs work directly.

## Domains

```sh
plinth domains.search --query acme                  # acme.com, .dev, .app, .io ... with prices
plinth domains.buy --env production --domain acme.dev   # owner approves the exact price
plinth domains.connect --env production --host app.acme.co   # a domain you already own
```

`domains.buy` registers the domain, points it at Plinth and serves it over HTTPS (apex and www). Only standard registrations are sold, no premium names. If the price changes before approval, the purchase is refused and you ask again.

## Payments

Purchases (like `domains.buy`) charge the owner's saved card after they approve the exact price.

```sh
plinth billing.status        # card on file? spend this month, cap, recent charges
plinth billing.card          # no card: give the owner this link; they add it on Stripe
plinth project.upgrade --project my-app     # Pro, $19/month per project; owner approves.
                                            # Pro: own domains, 1 GB / 1 CPU, 30-day backups, 90-day analytics,
                                            # a preview per branch, uptime emails
plinth project.downgrade --project my-app   # back to Free
```

Custom domains (`domains.buy`, `domains.connect`) need the project on Pro. Charges and subscription changes are prorated and billed to the owner's card.

If the card is declined, nothing is bought. If a purchase fails after the charge, it is refunded automatically.

## Approvals

These return `{"status":"pending_approval","approve_url":…}` until the owner taps Approve:
project.create, project.delete, promote to production, destructive migrations or SQL on production, db.restore on production, domains.buy.
The CLI and MCP server open `approve_url` in your human's browser automatically (the result says `opened_in_browser: true`); tell them it's waiting, then poll `approval.status` (the CLI waits for you). If your human asked you not to open things, pass `--no-open` (CLI) or `no_open: true` (MCP), or run `plinth config open_approvals false` once. No desktop (CI, SSH, headless Linux) means nothing opens: send them the link instead. The result of the action is in the approval.

When the owner wants to see their projects in the browser, run `plinth console` (or the `console.signin` tool) and give them the link. Approving it signs their browser into the console for 12 hours; the link expires after 15 minutes.

## All actions

`GET https://tryplinth.dev/v1/tools` returns every action with its JSON schema. Call one with
`POST /v1/actions/<name>` and `Authorization: Bearer <token>`, or `plinth <name> --key value`.
Every response is JSON: `{"ok":true,"result":…}` or `{"ok":false,"error":"code","message":"what to do"}`.
