Skip to content

@objectstack/spec/cloud declares camelCase rows but the /api/v1/cloud/* control plane speaks snake_case — the cloud contracts are not the wire types, so 15 client methods cannot be bound to them #12036

Description

@os-zhuang

Measured at c804f0ca5 while implementing #11925, which expected the cloud families to be bindable
("InstalledPackage … 20 of the 38 plausibly land on a type that exists today") and found they are not.
Filed rather than fixed: choosing a casing is a contract decision for domain:spec / the cloud control
plane, and packages/spec is read-only from a domain:cli seat.

The measurement

packages/spec/src/cloud/environment.zod.ts declares EnvironmentSchema entirely in camelCase:
displayName, organizationId, isDefault, isSystem, databaseUrl, databaseDriver, createdBy.

$ grep -rcoE "^\s+[a-z]+_[a-z_]+:" packages/spec/src/cloud/tenant.zod.ts \
packages/spec/src/cloud/environment.zod.ts packages/spec/src/cloud/environment-package.zod.ts
packages/spec/src/cloud/tenant.zod.ts:0
packages/spec/src/cloud/environment.zod.ts:0
packages/spec/src/cloud/environment-package.zod.ts:0
# reverse-check — a term independently present, not a substring of the term under test
$ grep -cE "^\s+[a-zA-Z]+:" packages/spec/src/cloud/environment.zod.ts
63

Zero snake_case keys across the three cloud row schemas, against 63 key lines in the same file — so the
zero is a real absence, not a broken pattern.

The in-repo consumers of those same routes read and write snake_case, on both request and response:

filereads / writes
packages/cli/src/commands/environments/show.ts:52-64p.display_name, p.organization_id, p.is_default, p.is_system, res.database.database_url, res.membership.role
packages/cli/src/commands/environments/list.ts:53-72p.display_name, p.is_default, p.is_system
packages/cli/src/commands/environments/create.ts:84-91sends organization_id, display_name, clone_from_environment_id

These call client.projects.get/list/create/activate/update, i.e. /api/v1/cloud/environments/*. They were
written against the live control plane, so the wire is snake_case and Environment is not its type.

Consequence

Binding the 15 unannotated cloud client methods — projects.list/get/create/update/activate/ rotateCredential/updateHostname/updateVisibility/retryProvisioning (9) and projects.packages.list/install/ get/enable/disable/upgrade (6) — to Environment / EnvironmentCredential /
EnvironmentPackageInstallationtypechecks and is false, and would break the CLI call sites above at
compile time while telling them they are wrong when they are right. This is the same near-miss class #8140
recorded for SearchResult, at family scale.

It also makes one spec docblock's claim untrue as written: ListEnvironmentPackagesResponseSchema
(environment-package.zod.ts:185) names GET /cloud/environments/:environmentId/packages as its route and
declares camelCase EnvironmentPackageInstallation rows.

The fork

  • The spec is right and the control plane should emit camelCase — one wire change in the cloud repo,
    then all 15 methods bind and the CLI's reads move with it.
  • The wire is right and the spec should declare snake_case — the cloud row schemas are the ones that move.
  • Both are right at different layers and the control plane is doing a documented case transform, in which
    case the transform needs to be stated somewhere a consumer can read.

Nothing here is guessable from this repo; the control-plane implementation is not in it (grep -rn "sessionUpdated" packages/ apps/ finds only the client's own declaration).

Related: #11925, #11924.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions