Amba

Collections as Code

Keep your collection schema in a file in your repo, diff it against the live project, apply additive changes, and make function deploys fail when a column your code writes is missing.

Server code that writes to collections (an Amba function, your own backend) assumes the columns it writes exist. Tests that run against an in-memory fake never check that, so a column you forgot to add only fails in production. Collections as code closes that gap: you declare the schema in a file next to your code, and Amba checks the live project against it before a function deploy goes live.

The schema file

Keep amba.collections.json at the root of your repo, next to amba.config.json if you use environment promotion. Each entry is the same shape amba_collections_create and POST /v1/admin/projects/:projectId/collections accept:

amba.collections.json
{
  "version": 1,
  "collections": [
    {
      "name": "scores",
      "columns": [
        { "name": "points", "type": "integer" },
        { "name": "level", "type": "text", "nullable": true },
        { "name": "embedding", "type": "vector", "dimension": 1536, "nullable": true }
      ],
      "indexes": [{ "columns": ["points desc"] }, { "columns": ["level", "points"] }],
      "shared": false,
      "owner_scope": "user",
      "read_policy": "public",
      "write_policy": "owner"
    }
  ]
}
FieldMeaning
columnsColumn name, type, nullable (default false), dimension for vector. Server-managed columns are never listed.
indexescolumns ("col" or "col desc"), optional unique, optional name, optional vector spec.
sharedtrue when rows may have no user_id (global content).
owner_scopeuser or group. Fixed when the collection is created.
read_policy / write_policyThe access policy, as on collection create, including write_policy: "server". Omit a field to leave it unmanaged.

id, user_id, group_id, created_at, updated_at and deleted_at are added to every collection automatically and never appear in the file.

Adopt it in one step

An existing project writes its live schema to the file:

amba collections pull

Commit the file. From then on, change the schema by editing the file and applying it.

Diff

amba collections diff [--file amba.collections.json]

diff prints two lists:

  • Missing from the live project. Collections, columns, indexes, the shared flag, NULL allowed on a column the file declares nullable that is NOT NULL live, and policies the file declares and the live project lacks. apply adds these.
  • Differences apply leaves alone. A column type or vector dimension that differs, a column the file declares NOT NULL that is nullable live, an owner scope that differs, a foreign key that differs, an index whose definition differs (uniqueness, column order or direction, vector method or distance), and columns, indexes or collections only the live project has. Each is marked [blocks deploy] when code written against the file can fail against the live project: a type mismatch, or a unique index live that the file declares non-unique.

diff exits with status 1 when the live project is missing anything the file declares, so it works as a CI gate.

Apply

amba collections apply [--file amba.collections.json] [--yes]

apply shows the plan, asks for confirmation (pass --yes in CI or any non-interactive shell), and then:

  • creates missing collections with their columns, indexes and policy;
  • adds missing columns and indexes to existing collections;
  • makes a collection shared when the file declares shared: true;
  • drops NOT NULL on a column the file declares nullable (drop_not_null, metadata-only and instant);
  • sets a declared read_policy / write_policy that differs from the live one, including write_policy: "server" for tables only backend code writes.

Apply is additive. It never drops a column, index or collection, never changes a column's type, and never adds NOT NULL, since existing NULLs would make that fail. Those differences stay listed so you can decide on them yourself: amba_collections_alter with change_type or set_not_null. set_not_null refuses with 409 NULLS_PRESENT and the NULL count while any row holds NULL in the column.

A column added to a collection that already has rows is added as nullable, because existing rows have no value for it. If the file declares it NOT NULL, the next diff lists that nullability difference. It does not block a deploy, since the live column accepts every value the file allows. Once the rows have values, run set_not_null to match the file.

Changes run in order and stop at the first failure. A failure (for example a unique index over rows that already hold duplicates) leaves everything applied before it in place, and running apply again picks up where it stopped. A unique index whose build failed is listed as missing until it is rebuilt; once the duplicate rows are fixed, apply drops the unusable index and builds it again. New collections are created in foreign-key order, so a collection is created after the collections its columns reference.

Function deploys check the schema

When amba.collections.json exists in the directory you deploy from, amba functions deploy diffs it against the live project before uploading. If the live project is missing anything the file declares, the deploy stops and prints the diff.

amba functions deploy functions/submit-score.ts                  # fails if a declared column is missing
amba functions deploy functions/submit-score.ts --apply-schema   # applies additive changes first
amba functions deploy functions/submit-score.ts --skip-schema-check
FlagEffect
--apply-schemaApplies the additive changes, then deploys. Still stops on a difference marked [blocks deploy].
--skip-schema-checkDeploys without reading the schema file.
--schema-file <path>Checks a different file. The deploy fails when that file does not exist.

--dry-run deploys stay local and skip the check.

For agents (MCP)

Two tools mirror the CLI. Both take the file's collections array as collections.

  • amba_collections_diff returns operations (what apply would add), differences (what it leaves alone, each with blocks_deploy), deploy_ready and in_sync. Pass include_live: true to also get the live schema in file shape.
  • amba_collections_apply applies the additive operations. Called without confirm: true it returns the plan and changes nothing; show it to the person you work with, then repeat the call with confirm: true.

API

Both endpoints take { "version"?: 1, "collections": [...] }.

POST /v1/admin/projects/:projectId/collections/diff

Optional include_live: true adds live, the current schema in file shape.

{
  "data": {
    "in_sync": false,
    "deploy_ready": false,
    "operations": [
      {
        "collection": "scores",
        "op": "add_column",
        "summary": "add column \"scores.level\" text",
        "body": { "add_column": { "name": "level", "type": "text", "nullable": true } }
      },
      {
        "collection": "scores",
        "op": "drop_not_null",
        "summary": "allow null in \"scores.run_id\" (drop NOT NULL)",
        "body": { "drop_not_null": "run_id" }
      }
    ],
    "differences": [
      {
        "collection": "scores",
        "column": "points",
        "kind": "nullability_mismatch",
        "declared": "not null",
        "live": "nullable",
        "blocks_deploy": false,
        "message": "Column is nullable live and NOT NULL in the file. Apply leaves it nullable, since existing rows may hold NULL. Require a value with amba_collections_alter set_not_null, which refuses with the NULL count when any rows hold NULL."
      }
    ]
  }
}

op is one of create_collection, add_column, add_index, relax_user_id, drop_not_null, set_policy; each body is the matching collection create or PATCH body. kind is one of type_mismatch, vector_dimension_mismatch, nullability_mismatch, owner_scope_mismatch, shared_mismatch, column_not_declared, index_not_declared, index_mismatch, foreign_key_mismatch, collection_not_declared.

POST /v1/admin/projects/:projectId/collections/apply

Optional dry_run: true returns the plan without changing anything.

{
  "data": {
    "dry_run": false,
    "applied": [
      {
        "collection": "scores",
        "op": "add_column",
        "summary": "add column \"scores.level\" text",
        "version": 12
      }
    ],
    "diff": { "...": "the diff apply started from" },
    "remaining": { "...": "the diff after the changes" }
  }
}

Errors: 400 INVALID_COLLECTIONS_SCHEMA for a malformed schema, 400 SCHEMA_APPLY_FAILED when an operation fails (details.applied lists what ran, details.failed names the operation and its error), 503 PROJECT_NOT_READY while the project is still provisioning.

On this page