# cloudResources (/docs/reference/api/cloud-resources)

> Cloud resources are a provider's managed data and platform services (managed databases and what lives in them, App Platform apps, Functions, Marketplace add-ons, partner attachments), served by the `@better-compute/cloud-resources` module through the provider-neutral `ProviderResources` contract. One table, one reconciler and these calls serve every kind a provider lists: DigitalOcean today (`do.database.pg`, `do.app`, …), AWS and GCP kinds later under their own prefixes. A resource belongs to one provider pool, so to that pool's account. Two hosting models use the same calls: a platform account's pool offered to the tenant (the resource lives on the platform's account, which other tenants share), or a pool on the tenant's own account. The calls record what the tenant wants and return at once; the `cloud.resources` reconciler then creates the resource at the provider, applies changes in place and deletes what the tenant deleted. `state` shows where it is: `pending`, `provisioning`, `ready`, `updating`, `deleting`, `deleted`, `released` or `error` (with a `message`). Resources still converging are looked at every 30 seconds; ready and failed ones are refreshed every five minutes, which retries an error and picks up what changed at the provider.



Cloud resources are a provider's managed data and platform services (managed databases and what lives in them, App Platform apps, Functions, Marketplace add-ons, partner attachments), served by the `@better-compute/cloud-resources` module through the provider-neutral `ProviderResources` contract. One table, one reconciler and these calls serve every kind a provider lists: DigitalOcean today (`do.database.pg`, `do.app`, …), AWS and GCP kinds later under their own prefixes. A resource belongs to one provider pool, so to that pool's account. Two hosting models use the same calls: a platform account's pool offered to the tenant (the resource lives on the platform's account, which other tenants share), or a pool on the tenant's own account. The calls record what the tenant wants and return at once; the `cloud.resources` reconciler then creates the resource at the provider, applies changes in place and deletes what the tenant deleted. `state` shows where it is: `pending`, `provisioning`, `ready`, `updating`, `deleting`, `deleted`, `released` or `error` (with a `message`). Resources still converging are looked at every 30 seconds; ready and failed ones are refreshed every five minutes, which retries an error and picks up what changed at the provider.

A resource's provider-side name is `bc-r-` and 24 characters of its id, and it carries the ownership labels `better-compute-managed`, `better-compute-resource` and `better-compute-tenant` (as tags where the provider has no labels). Secret outputs (a database password, a connection URI that embeds it, a namespace key) are sealed into the secret store in the reconciler run that received them; a resource returns only their `SecretRef`s in `secretOutputs`, and the values never reach a row, an event or a log. A resource bills `cloud.resource.hours` (one per hour, to the tenant) from the moment the provider first accepted it (an adopted one from its first `ready`) until it is deleted or released, on pools whose billing mode is `metered` or `platform-fee` (a resource living in another, such as a database user, pool, firewall or replica, bills no fee of its own); a kind's own meters bill only when the operator names them in `cloudResources({ kindMeters })`.

Every call is decided on `compute-cloud-resource/{id}` (or the tenant, for listings) with the attributes `kind`, `family` (`data`, `platform`, `ai`, `storage`, `network`, `edge`, `observability`), `provider`, `providerAccountId`, `poolId`, `region` and `projectId`. One action set serves every family; a policy narrows it by attribute: `StringEquals: { 'resource.family': 'data' }` allows a team managed databases without allowing App Platform.

All 11 methods are served by @better-compute/server 0.3.0.

| Method                  | Kind     | Authorization                                               | Phase | Status     |
| ----------------------- | -------- | ----------------------------------------------------------- | ----- | ---------- |
| [`kinds`](#kinds)       | read     | `compute:cloud-resources:read` @ `compute-tenant`           | v1.0  | registered |
| [`catalog`](#catalog)   | read     | `compute:cloud-resources:read` @ `compute-tenant`           | v1.0  | registered |
| [`signals`](#signals)   | read     | `compute:cloud-resources:read` @ `compute-tenant`           | v1.0  | registered |
| [`create`](#create)     | mutation | `compute:cloud-resources:create` @ `compute-cloud-resource` | v1.0  | registered |
| [`list`](#list)         | read     | `compute:cloud-resources:read` @ `compute-tenant` + items   | v1.0  | registered |
| [`get`](#get)           | read     | `compute:cloud-resources:read` @ `compute-cloud-resource`   | v1.0  | registered |
| [`update`](#update)     | mutation | `compute:cloud-resources:update` @ `compute-cloud-resource` | v1.0  | registered |
| [`delete`](#delete)     | mutation | `compute:cloud-resources:delete` @ `compute-cloud-resource` | v1.0  | registered |
| [`discover`](#discover) | read     | `compute:cloud-resources:read` @ `compute-tenant`           | v1.0  | registered |
| [`adopt`](#adopt)       | mutation | `compute:cloud-resources:create` @ `compute-cloud-resource` | v1.0  | registered |
| [`release`](#release)   | mutation | `compute:cloud-resources:delete` @ `compute-cloud-resource` | v1.0  | registered |

## kinds [#kinds]

`POST /api/compute/cloudResources/kinds`

Returns the kinds the pool's provider serves: for each its `family`, the JSON Schema of its `spec`, the plain `outputs` and `secretOutputs` it reports, whether it is `regional`, which spec fields are `updatable` in place, its `meters`, its `parent` kind (a database user lives in a cluster), whether it is a provider `preview` and `available` on this account (previews need `previews: true` on the account), and the tenant's `quota` of it. A pool that is not a provider pool, or whose provider serves no cloud resources, is `UNSUPPORTED` (`NOT_FOUND` for a pool the tenant cannot see). Decided as `compute:cloud-resources:read` on the tenant.

* **Kind:** read
* **Authorization:** `compute:cloud-resources:read` @ `compute-tenant`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type   | Required | Constraints                                                       |
| ---------- | ------ | -------- | ----------------------------------------------------------------- |
| `tenantId` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`   | string | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |

## catalog [#catalog]

`POST /api/compute/cloudResources/catalog`

Returns the installable applications the pool's provider lists (DigitalOcean's 1-Click apps), at most 1000, optionally only those with `installsOn` `instance` (the slug is the image of an instance create) or `kubernetes` (an add-on of a managed cluster). Read-only: they are installed through those surfaces, never through `create`. The provider is asked with no transaction open. A provider without a catalog is `UNSUPPORTED`. Decided as `compute:cloud-resources:read` on the tenant.

* **Kind:** read
* **Authorization:** `compute:cloud-resources:read` @ `compute-tenant`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field        | Type                            | Required | Constraints                                                       |
| ------------ | ------------------------------- | -------- | ----------------------------------------------------------------- |
| `tenantId`   | string                          | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`     | string                          | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `installsOn` | one of `instance`, `kubernetes` |          |                                                                   |

## signals [#signals]

`POST /api/compute/cloudResources/signals`

Returns the read-only signals the pool's provider raises about its resources (DigitalOcean's Managed Agents Signals: a run that failed, an agent asking for a human), newest first, at most 500, for an attention list. Each has the provider's `id`, a `severity` (`critical`, `warning` or `info`), a one-line `title`, an optional `detail` (one line, at most 500 characters), `at` (epoch ms), the `kind` and `providerId` of the resource it is about, and the `resourceId` of the cloud resource that records it, if any; `resourceId` in the input narrows it to that resource. A tenant of a shared pool sees only the signals about its own resources; the pool's owner (the root tenant for the platform's pool, a tenant on its own account) sees every signal, recorded or not. The provider is asked with no transaction open and sends no mutation; malformed signals are dropped. One answer of the provider account is reused for 30 seconds, whichever tenant asked. A provider without signals is `UNSUPPORTED`, as is a provider that holds them behind previews on an account without `previews: true` (`details.reason` `preview`). Decided as `compute:cloud-resources:read` on the tenant.

* **Kind:** read
* **Authorization:** `compute:cloud-resources:read` @ `compute-tenant`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field        | Type   | Required | Constraints                                                       |
| ------------ | ------ | -------- | ----------------------------------------------------------------- |
| `tenantId`   | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`     | string | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `resourceId` | string |          | at most 56 characters; matches `^crs_[0-9a-hjkmnp-tv-z]{26,52}$`  |

## create [#create]

`POST /api/compute/cloudResources/create`

Records a resource and returns it `pending`.

* The pool must be visible to the tenant and a `provider` pool whose provider serves the `kind`; otherwise `UNSUPPORTED` (`NOT_FOUND` for a pool the tenant cannot see). A preview kind on an account without previews, or a kind with secret outputs on a deployment without a secret store, is `UNSUPPORTED`.
* `spec` (at most 64 KiB of JSON) is validated by the kind's manifest (`INVALID_INPUT`, with the field under `spec.`). A regional kind's `spec.region` must be a region the account configures. A secret in a spec is a `SecretRef` to the tenant's own secret (`secret://<tenant>/…`), resolved only just before the provider call.
* On a shared (hosted) account, a spec that reaches account-wide objects (a VPC, a container registry, linked repositories, a database cluster by name, Droplet tags or ids as trusted sources) is `INVALID_INPUT` with `details.reason` `accountWide` and the fields; on the tenant's own account it may. A spec may instead name the tenant's instances (a database's trusted `instances`): each must be a live instance of the tenant on the pool's account, otherwise `INVALID_INPUT` with `details.reason` `instance` (the same for one that does not exist). A spec may also name the tenant's networks (a database cluster's `network`, which joins that network's VPC in the cluster's region): each must be a live network of the tenant on the pool's account, otherwise `INVALID_INPUT` with `details.reason` `network` and the `networks`; the reconciler checks them again before every provider call.
* A kind with a parent names it in `parentId`: a resource of the parent kind on the same account, not being deleted. A kind that declares an identity within its parent (a database user's name; a cluster's single firewall) allows one live resource of it per parent: a second is `CONFLICT` with `details.reason` `identity` and the `resourceId` that has it.
* `name` is 1 to 63 characters, `[a-z]([-a-z0-9]*[a-z0-9])?`, unique per kind among the tenant's resources that are not deleted or released (`CONFLICT` otherwise).
* A tenant that already has its quota of the kind (the module's `maxPerKind`, default 20, or a per-kind `quotas` entry; one being deleted still counts) is `QUOTA_EXCEEDED`, with the cap in `details.limit`.
* A kind that books capacity quota (a DigitalOcean Dedicated Inference endpoint books its GPUs: GPUs per replica times the larger of `replicas` and `maxReplicas`) books it against the tenant's quota on the pool in the same transaction that records the resource. Over the quota is `QUOTA_EXCEEDED` and nothing is recorded.
* Decided as `compute:cloud-resources:create` on the resource it would create, with its kind and family as attributes. A refusal leaves nothing behind. After the decision the tenant's offer is checked (a suspended offer or an excluded region refuses), and the tenant's budgets unless the pool's billing mode is `none`.

The resource id derives from the tenant, the caller and `idempotencyKey`. A retry with the same key returns the same resource; the same key with any other field changed is `CONFLICT`. The caller owns the resource. Publishes `cloud.resource.created`, then `cloud.resource.ready` the first time it is ready (with the names of its outputs and secret outputs) or `cloud.resource.failed`.

* **Kind:** mutation
* **Authorization:** `compute:cloud-resources:create` @ `compute-cloud-resource`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field            | Type   | Required | Constraints                                                        |
| ---------------- | ------ | -------- | ------------------------------------------------------------------ |
| `tenantId`       | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |
| `projectId`      | string |          | at most 56 characters; matches `^prj_[0-9a-hjkmnp-tv-z]{26,52}$`   |
| `poolId`         | string | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `kind`           | string | yes      | at most 64 characters; matches `^[a-z0-9]+(?:[.-][a-z0-9]+)*$`     |
| `name`           | string | yes      | at most 63 characters; matches `^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$` |
| `description`    | string |          | at most 256 characters                                             |
| `parentId`       | string |          | at most 56 characters; matches `^crs_[0-9a-hjkmnp-tv-z]{26,52}$`   |
| `spec`           | object | yes      |                                                                    |
| `idempotencyKey` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |

## list [#list]

`POST /api/compute/cloudResources/list`

Returns the tenant's resources, oldest first; `poolId`, `projectId`, `kind` and `family` narrow it, and `includeDeleted: true` adds deleted and released ones. At most 500 are returned. Decided as `compute:cloud-resources:read` on the tenant.

* **Kind:** read
* **Authorization:** `compute:cloud-resources:read` @ `compute-tenant` + items
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field            | Type                                                                           | Required | Constraints                                                       |
| ---------------- | ------------------------------------------------------------------------------ | -------- | ----------------------------------------------------------------- |
| `tenantId`       | string                                                                         | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`         | string                                                                         |          | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `projectId`      | string                                                                         |          | at most 56 characters; matches `^prj_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `kind`           | string                                                                         |          | at most 64 characters; matches `^[a-z0-9]+(?:[.-][a-z0-9]+)*$`    |
| `family`         | one of `storage`, `network`, `edge`, `data`, `platform`, `ai`, `observability` |          |                                                                   |
| `includeDeleted` | boolean                                                                        |          | default `false`                                                   |

## get [#get]

`POST /api/compute/cloudResources/get`

Returns one resource with its spec, `providerId` once the provider accepted it, `outputs`, `secretOutputs` (SecretRefs only) and state. A resource of another tenant is `NOT_FOUND` here, and is refused by the decision (`ACCESS_DENIED`) before that. Decided as `compute:cloud-resources:read` on the resource.

* **Kind:** read
* **Authorization:** `compute:cloud-resources:read` @ `compute-cloud-resource`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field        | Type   | Required | Constraints                                                      |
| ------------ | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`   | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `resourceId` | string | yes      | at most 56 characters; matches `^crs_[0-9a-hjkmnp-tv-z]{26,52}$` |

## update [#update]

`POST /api/compute/cloudResources/update`

Merges `spec` onto the stored spec and validates the result with the kind's manifest; `description` is better-compute's alone. A change to the kind's `updatable` fields is applied in place: a `ready` or failed resource goes `updating` and back to `ready` (`cloud.resource.updating`, then `cloud.resource.updated`). A change to any other field is `INVALID_STATE` with `details.reason` `replaceRequired` and the fields: create a new resource instead. The account-wide rule of `create` applies to the merged spec, and an instance or network the change newly names is checked as on `create`. For a kind that books quota, the booking follows the spec: growth is charged in the same transaction (`QUOTA_EXCEEDED` refuses the update and changes nothing); a shrink is returned once the provider reports the new spec applied (the resource is `ready` again), since until then the old amount still runs. An adopted resource's spec cannot change before its takeover completed (`INVALID_STATE`, `adopting`); its description can. A resource being deleted or released is `INVALID_STATE`. Every update bumps `version`. Decided as `compute:cloud-resources:update` on the resource.

* **Kind:** mutation
* **Authorization:** `compute:cloud-resources:update` @ `compute-cloud-resource`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field         | Type        | Required | Constraints                                                      |
| ------------- | ----------- | -------- | ---------------------------------------------------------------- |
| `tenantId`    | string      | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `resourceId`  | string      | yes      | at most 56 characters; matches `^crs_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `spec`        | object      |          |                                                                  |
| `description` | string,null |          | at most 256 characters                                           |

## delete [#delete]

`POST /api/compute/cloudResources/delete`

Marks the resource and every resource living in it for deletion and returns it `deleting`. The reconciler deletes the children first (a child created while the parent was being deleted is deleted too), then the resource at the provider; it is then `deleted`, its authorization row is removed, its sealed secrets are destroyed, the quota its spec booked is returned (once) and `cloud.resource.deleted` is published. A resource the provider never accepted is deleted without a provider call. A resource this deployment does not manage at the provider is never deleted there: it stays in `error`. An adopted resource whose takeover never completed is let go instead (`released`, with no provider call). Deleting a resource that is being deleted returns it unchanged. Deleting one that is being released overrides the release: it is deleted instead (should the provider already have let it go, the delete is refused there as not managed and nothing is destroyed). Publishes `cloud.resource.deleting`. Decided as `compute:cloud-resources:delete` on the resource.

* **Kind:** mutation
* **Authorization:** `compute:cloud-resources:delete` @ `compute-cloud-resource`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field        | Type   | Required | Constraints                                                      |
| ------------ | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`   | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `resourceId` | string | yes      | at most 56 characters; matches `^crs_[0-9a-hjkmnp-tv-z]{26,52}$` |

## discover [#discover]

`POST /api/compute/cloudResources/discover`

Lists the kind's resources on the tenant's own account (at most 500): each with its `providerId`, `name`, `region`, `spec` as the provider reports it, `managedBy` (`this` deployment, an `other` one, or absent: none) and the `resourceId` of the cloud resource that records it, if any. A pool on an account of another tenant (a platform pool offered to the tenant) is `ACCESS_DENIED` with `details.reason` `notAccountOwner`. The provider is asked with no transaction open. Decided as `compute:cloud-resources:read` on the tenant.

* **Kind:** read
* **Authorization:** `compute:cloud-resources:read` @ `compute-tenant`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type   | Required | Constraints                                                       |
| ---------- | ------ | -------- | ----------------------------------------------------------------- |
| `tenantId` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`   | string | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `kind`     | string | yes      | at most 64 characters; matches `^[a-z0-9]+(?:[.-][a-z0-9]+)*$`    |

## adopt [#adopt]

`POST /api/compute/cloudResources/adopt`

Takes over an unmanaged resource of the tenant's own account and returns it `pending`, `adopted: true`, with an empty spec. Before anything is recorded the provider is asked for that one resource: one that does not exist is `NOT_FOUND`, one another deployment manages is `INVALID_STATE` with `details.reason` `managedByAnother`. A kind that lives in a parent cannot be adopted (adopt the parent and create its children); a kind that books quota (a Dedicated Inference endpoint) is `UNSUPPORTED` with `details.reason` `quotaBooked`, since nothing was charged for what it runs on (create it through better-compute instead); and a resource already recorded, or a name taken, is `CONFLICT`. The reconciler writes the ownership labels in its first run (the provider refuses there too a resource another deployment took meanwhile) and records the spec the provider reports; from then on it is observed until the tenant changes its spec, then managed like any other. A platform account is `ACCESS_DENIED` as for `discover`. Publishes `cloud.resource.adopted`. Decided as `compute:cloud-resources:create`, as for `create`.

* **Kind:** mutation
* **Authorization:** `compute:cloud-resources:create` @ `compute-cloud-resource`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field            | Type   | Required | Constraints                                                        |
| ---------------- | ------ | -------- | ------------------------------------------------------------------ |
| `tenantId`       | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |
| `projectId`      | string |          | at most 56 characters; matches `^prj_[0-9a-hjkmnp-tv-z]{26,52}$`   |
| `poolId`         | string | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `kind`           | string | yes      | at most 64 characters; matches `^[a-z0-9]+(?:[.-][a-z0-9]+)*$`     |
| `providerId`     | string | yes      | at most 200 characters                                             |
| `name`           | string | yes      | at most 63 characters; matches `^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$` |
| `description`    | string |          | at most 256 characters                                             |
| `idempotencyKey` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |

## release [#release]

`POST /api/compute/cloudResources/release`

Lets a resource go: the reconciler removes its ownership labels at the provider and leaves it running, and the resource is `released` (its authorization row removed, its sealed secrets destroyed, its quota booking returned, `cloud.resource.released` published). A resource the provider has not accepted yet is `INVALID_STATE` (delete it instead), as is one that lives in a parent (release that) or one that still has live children. A kind the provider cannot release (one owned by its name rather than by labels, such as a DigitalOcean Functions namespace or model access key) is `UNSUPPORTED` with `details.reason` `release`, and nothing is recorded: keep it or delete it. Should a provider refuse only when the reconciler asks, the release of a resource better-compute created is withdrawn (it stays managed, in `error` with the reason until the next refresh), and an adopted one is let go without the provider call. Publishes `cloud.resource.releasing`. Decided as `compute:cloud-resources:delete` on the resource.

* **Kind:** mutation
* **Authorization:** `compute:cloud-resources:delete` @ `compute-cloud-resource`
* **Module:** `cloud-resources`
* **Phase:** v1.0 (registered)

**Input**

| Field        | Type   | Required | Constraints                                                      |
| ------------ | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`   | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `resourceId` | string | yes      | at most 56 characters; matches `^crs_[0-9a-hjkmnp-tv-z]{26,52}$` |
