# 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://plinth.2.31.19.96.sslip.io

## Install

```sh
curl -fsSL https://plinth.2.31.19.96.sslip.io/_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://plinth.2.31.19.96.sslip.io/_plinth/cli.tgz", "mcp"],
  "env": { "PLINTH_URL": "https://plinth.2.31.19.96.sslip.io" } } } }
```

## 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. 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.

## 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.
Give the owner `approve_url`, then poll `approval.status` (CLI waits for you). The result of the action is in the approval.

## All actions

`GET https://plinth.2.31.19.96.sslip.io/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"}`.
