# `3000cloud.json` — manifest reference (preview)

The manifest is the single deploy input: your agent writes it at the repo root, the platform deploys from it. JSON Schema: https://3000cloud.com/schema/v1.json. Unknown keys are rejected everywhere (strict objects). Every validation error is `{path, message, hint}` — fix each path per its hint and resubmit.

> **Preview status.** The validator enforces the full schema below, but the preview cluster honors a subset. **Honored today:** `name`; `services` with `type: "web"` on **node**/**python** runtimes — including both at once for a frontend-build + backend shape (`root`, `install`, `build`, `start`, `port`, `healthCheckPath`) and `type: "static"` — `install`/`start` run in-cluster when the app boots (no Dockerfile builds yet); `volumes[]` node-local persistence. **Validated but NOT yet in effect** (marked *(soon)* below): worker services, `cron[]`, `db`, `access` modes, secret `env` handling, `aptPackages`, `preDeploy`, `region: "eu"`, `lockfileHashes` verification. Fields you set there are accepted and stored, but do not rely on their behavior yet.

## Top level

| Field | Required | Meaning |
|---|---|---|
| `schemaVersion` | yes | Always `1`. |
| `name` | yes | App name; lowercase DNS label. Becomes `https://<name>.3000cloud.app`. |
| `region` | no | `"us"` (default). `"eu"` *(soon)*. |
| `services` | yes | 1–5 services (below). At most **one** `web` service. |
| `env` | no | Env var specs (below). Secret handling *(soon)* — see the env section. |
| `volumes` | no | Persistent disks, e.g. `[{"mountPath": "/data", "sizeGB": 5}]` — node-local storage mounted at `mountPath`. Survives restarts and redeploys, but is pinned to the machine it was created on; **no backups yet**. |
| `db` | no | *(soon)* `{"kind": "sqlite", "path": "/data/app.db"}` or `{"kind": "postgres"}` (+$5/mo add-on at launch). |
| `cron` | no | *(soon)* Scheduled jobs; max 20, unique names, 5-field UTC cron, command runs without a shell. |
| `egress` | no | FQDN allowlist; default `[]` = no outbound network. Hostnames only (`"api.openai.com"`, one leading `"*."` label allowed, never bare `"*"`). The cluster is default-deny — list every external API your app calls. |
| `access` | no | *(soon)* Defaults to `{"mode": "secret-link"}` in the schema, but the gate is not live: **during the preview apps are reachable by anyone with the URL.** |
| `resources` | no | `{"tier": "starter" \| "base" \| "plus" \| "pro"}`. Default `starter`. Sizes your dedicated resources (requests = limits). See [pricing.md](https://3000cloud.com/docs/pricing.md). |
| `lockfileHashes` | no | *(soon)* Map of repo-relative lockfile path to `"sha256:<64 hex>"`. |

## `services[]`

| Field | Meaning |
|---|---|
| `name` | Unique DNS label. |
| `type` | `"web"` (default; routed + health-checked), `"static"` (serves prebuilt files), `"worker"` *(soon — validates but will not run yet)*. |
| `root` | Subdirectory the service lives in (repo-relative, no `..`). |
| `runtime` | Version map, e.g. `{"node": "22"}` or `{"python": "3.12"}`. The preview runs **node** and **python**; other keys (go, ruby, ...) validate but will not deploy yet. Declaring **both** node and python makes a multi-runtime bundle: node builds the frontend first (`build`, default `npm install && npm run build`), then python runs the backend (`install`, then `start`) — the backend should serve the built static dir itself. |
| `packageManager` | `pip \| uv \| poetry \| pipenv \| npm \| pnpm \| yarn \| bun` (advisory). |
| `aptPackages` | *(soon)* Debian packages for the build image. |
| `install` / `build` / `start` | Commands, run in-cluster at app boot. `start` is required for web services and **must bind `$PORT`** on `0.0.0.0` (or the literal declared `port`). |
| `port` | 1–65535; required for web services. |
| `healthCheckPath` | URL path (starts with `/`) polled for readiness; web only. |
| `preDeploy` | *(soon)* Runs once per deploy before the new version starts (migrations). |
| `framework` | Advisory slug (`fastapi`, `nextjs`, ...). |
| `static` | `{"dir": "...", "urlPrefix": "/"}` for static services. `dir` is relative to `root` and may use `../` (e.g. a built frontend). |

## `env`

Keys are `UPPER_SNAKE_CASE`. Each spec takes **exactly one** value source:

- `{"value": "literal"}` — non-secret config. **Never put real secrets here** — the manifest travels in plain requests.
- `{"generator": "secret"}` — *(soon)* platform-generated secret at first deploy.
- `{"fromDatabase": true}` — *(soon)* injected DB connection string (requires `db`).
- `{"required": true, "description": "..."}` — *(soon)* value supplied after deploy via a secret channel. **The secret upload channel is not live yet**: apps that need secret env vars cannot receive them in the preview — hold off deploying those, or ask the operator.

## `egress[]`

Hostnames only — `"api.stripe.com"`, or a single leading wildcard label `"*.googleapis.com"`. No schemes, paths, ports, IPs, or bare `"*"`. Everything not listed is unreachable (the cluster denies outbound by default).

## `resources`

`{"tier": "starter"}` (the default — $10/mo, 0.5 vCPU / 1 GiB / 5 GiB; `base` at $30/mo — 1 vCPU / 2 GiB — is the step up) — the priced menu is at `GET https://api.3000cloud.com/v1/tiers` (no auth) and [pricing.md](https://3000cloud.com/docs/pricing.md). Resources are dedicated: requests = limits, what you buy is what you get. (Billing itself is not live during the preview.)

## Validation errors

The API returns `{stage: "validation", message, hint, errors: [{path, message, hint}]}` — e.g. `path: "egress[0]"`, `message: '"https://api.stripe.com" is a URL, not a hostname'`, `hint: "egress entries must be hostnames like api.stripe.com — remove the URL scheme"`. Fix each `path` per its `hint` and resubmit.

> Keep-in-sync note: this page mirrors the `@3000cloud/manifest` package (`packages/manifest`), which is the source of truth for the schema and tier constants. If they disagree, the package wins.
