# Managed environments (/docs/environments/managed)

VMs that codai provisions for you on OpenStack or Hetzner — sizes, regions, start and stop, idle auto-stop and snapshots.

A **managed** environment is a VM that codai creates, enrols and looks after. You pick a provider and a size; a worker that runs every minute provisions the machine, installs `codaid` through cloud-init, stops it when idle and bills it per second of runtime.

<Callout type="info">
Managed providers are **rolling out** and enabled per account. Until yours is enabled, `POST /v1/environments` with a managed provider answers `403` — *Provider 'hetzner' is not available yet; use 'byo' (bring your own machine)* — and the hub shows the managed options as *coming soon*. [Bring your own server](./byo-install.mdx) works for everyone.
</Callout>

## Providers and regions [#providers-and-regions]

| `provider` | Where | Default `region` |
| --- | --- | --- |
| `openstack` | codai cloud on Cloudify (EU, Romania), Ubuntu 24.04 | `eu-east-1` |
| `hetzner` | Hetzner Cloud | `nbg1` (the machine types below exist in `fsn1`, `hel1` and `nbg1`) |

The `local` provider value exists in the schema but is reserved; it is not provisioned.

## Sizes [#sizes]

Each provider bills the RAM it actually gives you.

| `size` | vCPU | OpenStack (Cloudify) | Rate | Hetzner | Rate |
| --- | --- | --- | --- | --- | --- |
| `small` (default) | 2 | `b2i.2c-2g`, 2 GB | €0.03 / h | `cx23`, 4 GB | €0.04 / h |
| `medium` | 4 | `b2i.4c-4g`, 4 GB | €0.06 / h | `cx33`, 8 GB | €0.08 / h |
| `large` | 8 | `b2i.8c-8g`, 8 GB | €0.12 / h | `cx43`, 16 GB | €0.16 / h |

Rate = vCPU × €0.01/h + GB RAM × €0.005/h, stored on the environment at creation (`hourlyRateMicroEur`). The server type is a preference: if a provider retires it, codai picks the cheapest live type with at least the same vCPU and RAM and still bills the size's rate. Disk is whatever the flavor or server type ships with (OpenStack 20 / 40 / 80 GB, Hetzner 40 / 80 / 160 GB); while the machine is stopped or archived, the kept disk costs €0.05 per GB-month — see [storage](./billing.mdx#storage-for-stopped-and-archived-environments). GPU environments are coming soon.

Creating or starting a managed environment needs a wallet balance of at least one hour at its rate, otherwise `402 insufficient_balance` ([details](./billing.mdx#balance-check-on-create-and-start)).

### Compute reserve and grace [#compute-reserve-and-grace]

Set a **compute reserve** in Hub → Billing: LLM usage never spends the wallet below it, so a busy chat session cannot starve your VM. If a 10-minute bucket still cannot be paid, the environment enters a 30-minute **grace** period (shown in Hub and the desktop app with the stop time); a top-up during grace pays the missed bucket and clears it. After 30 minutes the environment is snapshot-stopped — nothing is lost.

## Create one [#create-one]

<Tabs items={['Hub', 'CLI', 'API']}>
<Tab value="Hub">

In [hub.codai.ro/projects](https://hub.codai.ro/projects), open a project → **New environment**, choose **codai cloud (OpenStack)** or **Hetzner**, a size, the idle timeout (5–1440 minutes) and an optional monthly budget cap in euro.

</Tab>
<Tab value="CLI">

```bash
codaid env create --project my-app --name ci-box --provider hetzner --size medium
codaid env status ci-box
```

</Tab>
<Tab value="API">

```bash
curl https://ai.codai.ro/v1/environments \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "project_id": "2b6d1c0e-4f3a-4b9c-8d7e-1a2b3c4d5e6f", "name": "ci-box",
        "provider": "hetzner", "size": "medium", "idle_timeout_minutes": 30, "budget_micro_eur": 20000000 }'
```

</Tab>
</Tabs>

The environment starts in `creating`. The worker creates the server (`creating` → `enrolling` once it has a public IP), cloud-init installs `codaid` as a system service and enrols it with a token the worker mints, and the environment becomes `running`. On both providers this took about two to three minutes in production. At enrolment the daemon of a managed machine receives its own API key under the environment owner; destroying the environment revokes it.

That daemon key is **scoped to its own environment**: it may call inference, drive the shared session of a task it runs, read its own environment (and its project), and list its own environment's secret names — nothing else. Any other route answers `403 key_scope_forbidden`, so a compromised machine cannot reach your other environments, keys or wallet.

| Step | Time limit | On timeout |
| --- | --- | --- |
| Create the server | 10 min | retried; after 3 consecutive provider failures the environment is parked in `error` |
| Daemon enrols | 15 min | `error`, reason `enroll_timeout` — a late enrolment still recovers it to `running` |
| Start from `stopped` | 10 min | `error` |

The worker never destroys a machine on an error path; you decide.

An environment in `error` whose server still exists is billed like a running one; after 30 minutes in `error` the worker snapshot-stops it, so a broken machine does not keep charging.

## Lifecycle [#lifecycle]

| Action | Allowed from | Goes to | Role |
| --- | --- | --- | --- |
| `stop` | `running` | `stopping` → `stopped` | maintainer |
| `start` | `stopped` | `starting` → `running` | maintainer |
| `archive` | `stopped` | `archiving` → `archived` | maintainer |
| `destroy` | any state except `destroying`/`destroyed` | `destroying` → `destroyed` | owner |

Anything else answers `409 invalid_transition`. Hub buttons, `codaid env start|stop|destroy` and `POST /v1/environments/{id}/{action}` all go through the same transition table.

What stop and start do on each provider:

| | Stop | Start |
| --- | --- | --- |
| **OpenStack** | Shelve, then shelve-offload: the server id and public IP are kept, compute is released. | Unshelve the same server. |
| **Hetzner** | Take a snapshot, then delete the server. | Create a new server from the snapshot. The previous snapshot is deleted after the next stop. |

`archive` runs the same stop path and parks the machine in `archived`; the only way out of `archived` is `destroy`. `destroy` deletes the server (and on Hetzner its snapshot) and revokes the daemon key.

## Idle auto-stop [#idle-auto-stop]

Every environment has `idle_timeout_minutes` (default **30**, 5–1440; change it with `PATCH /v1/environments/{id}` or in the hub). The worker stops a running managed environment when

```text
now − max(last_activity_at, started_at) > idle_timeout_minutes
```

with reason `idle`. **Activity** is:

- any exec, terminal or port-stream byte from an attached client, through the relay;
- daemon heartbeats reporting `human` or `agent` activity — an `idle` heartbeat never counts;
- requests the daemon itself makes to the gateway with its key (for example an agent loop running inside the machine);
- your own start/stop/archive/destroy requests.

Open preview traffic from third parties counts only as port-stream bytes, so a public preview with visitors keeps the machine awake. Activity is recorded at most once a minute.

The machine can also be stopped for billing reasons — `insufficient_funds`, `billing_error` or `budget_exceeded`; see [billing](./billing.mdx). The reason is shown on the environment page and returned as `stateReason`.

## Prebuilds [#prebuilds]

A project can keep a pool of up to **5** ready machines so a new environment starts from a warmed snapshot instead of a fresh install:

```bash
curl -X PATCH https://ai.codai.ro/v1/projects/$PROJECT_ID \
  -H "Authorization: Bearer $CODAI_API_KEY" -H 'content-type: application/json' \
  -d '{"prebuild_pool_size": 2, "prebuild_provider": "openstack", "prebuild_size": "small"}'
```

- Only the project owner or an org owner/admin can set it. The worker tops the pool up every minute, runs only the devcontainer's `onCreateCommand` and `updateContentCommand`, then **stops** each machine.
- The **project owner pays** the warm-up compute and the storage of the stopped pool machines (the normal [storage rate](./billing.mdx)). The pool is not topped up while the owner's wallet holds less than one hour of a pool machine.
- `POST /v1/environments` with the same provider and size (and no `slug`) claims a ready machine: ownership moves to you, it starts right away and the response carries `prebuild_claimed: true`. From then on you pay for it. The one-hour balance check still applies.
- Unclaimed pool machines are visible only to the project owner. Lowering `prebuild_pool_size`, or changing the provider/size, destroys the extra unclaimed machines.

## Coming soon [#coming-soon]

GPU environments and choosing the region in the hub are not available yet.
