cloudResources
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 SecretRefs 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 | read | compute:cloud-resources:read @ compute-tenant | v1.0 | registered |
catalog | read | compute:cloud-resources:read @ compute-tenant | v1.0 | registered |
signals | read | compute:cloud-resources:read @ compute-tenant | v1.0 | registered |
create | mutation | compute:cloud-resources:create @ compute-cloud-resource | v1.0 | registered |
list | read | compute:cloud-resources:read @ compute-tenant + items | v1.0 | registered |
get | read | compute:cloud-resources:read @ compute-cloud-resource | v1.0 | registered |
update | mutation | compute:cloud-resources:update @ compute-cloud-resource | v1.0 | registered |
delete | mutation | compute:cloud-resources:delete @ compute-cloud-resource | v1.0 | registered |
discover | read | compute:cloud-resources:read @ compute-tenant | v1.0 | registered |
adopt | mutation | compute:cloud-resources:create @ compute-cloud-resource | v1.0 | registered |
release | mutation | compute:cloud-resources:delete @ compute-cloud-resource | v1.0 | registered |
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
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
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
POST /api/compute/cloudResources/create
Records a resource and returns it pending.
- The pool must be visible to the tenant and a
providerpool whose provider serves thekind; otherwiseUNSUPPORTED(NOT_FOUNDfor 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, isUNSUPPORTED. spec(at most 64 KiB of JSON) is validated by the kind's manifest (INVALID_INPUT, with the field underspec.). A regional kind'sspec.regionmust be a region the account configures. A secret in a spec is aSecretRefto 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_INPUTwithdetails.reasonaccountWideand the fields; on the tenant's own account it may. A spec may instead name the tenant's instances (a database's trustedinstances): each must be a live instance of the tenant on the pool's account, otherwiseINVALID_INPUTwithdetails.reasoninstance(the same for one that does not exist). A spec may also name the tenant's networks (a database cluster'snetwork, 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, otherwiseINVALID_INPUTwithdetails.reasonnetworkand thenetworks; 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 isCONFLICTwithdetails.reasonidentityand theresourceIdthat has it. nameis 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 (CONFLICTotherwise).- A tenant that already has its quota of the kind (the module's
maxPerKind, default 20, or a per-kindquotasentry; one being deleted still counts) isQUOTA_EXCEEDED, with the cap indetails.limit. - A kind that books capacity quota (a DigitalOcean Dedicated Inference endpoint books its GPUs: GPUs per replica times the larger of
replicasandmaxReplicas) books it against the tenant's quota on the pool in the same transaction that records the resource. Over the quota isQUOTA_EXCEEDEDand nothing is recorded. - Decided as
compute:cloud-resources:createon 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 isnone.
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
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
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
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
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
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
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
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}$ |
Better Compute is created by Sean Filimon
Last updated
certificates
TLS certificates on a provider account (DigitalOcean first), for the tenant's [load balancers](/docs/reference/api/load-balancers). Every call records what the tenant wants and returns at once; the `vps.provider-resources` reconciler converges the provider through `ProviderImpl.certificates`. Reading takes `compute:certificates:read`, everything else `compute:certificates:manage`, on `provider-certificate/{id}`. Certificates book no quota (at most 100 per tenant) and do not bill.
commandBatches
Destructive-command batches (v1.0 quorum). A batch fixes a set of (subject, kind) leaves, optionally bound to exact payloads, under one Merkle root when it is created (modules create them through `commands.createBatch`). When the deployment publishes approver keys (`pki.approvers`), the batch's commands wait until enough approvers, holding keys off the control plane, have signed its root for the trust domain. The gateway then sends each command with the approval and its leaf's proof, and the daemon verifies both. Without approver keys a batch is recorded for audit and its commands go out at once. See [Security](/docs/operations/security).