Schema Migration

Operation Reference

The schema migration operations, grouped by the model entity they act on.

Operation Format

Every operation is an object with a kind and a data payload. Operations are grouped below by the entity they act on: namespaces, collections, fields, indexes, and relations.

New entities carry a client-generated UUID id that you reuse to reference them in later operations. Field and index operations nest inside createCollection and updateCollection; every other operation is top-level. Within a collection, Monospace sorts the nested field and index operations into a safe order. Top-level operations run in the order you send them, so order them yourself when one depends on another — create a collection before the relation that references it.

Collections and fields carry two names. dbName is the object's storage name in the data source. apiName is the name used in the data API: a collection is served at /items/{apiName}, and fields are addressed by apiName in query parameters. When apiName is omitted, it falls back to dbName.

Namespaces

A namespace is a schema in the data source that groups related collections.

Create a Namespace

createNamespace adds a namespace to a data source.

operation.json
{
  "kind": "createNamespace",
  "data": {
    "id": "3c1d2e3f-0001-4a1a-9c1a-000000000001",
    "sourceId": "d7c8f0e2-9b3a-4c1e-8f2a-1a2b3c4d5e6f",
    "dbName": "reporting"
  }
}

Rename a Namespace

renameNamespace changes a namespace's dbName.

operation.json
{
  "kind": "renameNamespace",
  "data": {
    "id": "3c1d2e3f-0001-4a1a-9c1a-000000000001",
    "dbName": "analytics"
  }
}

Delete a Namespace

deleteNamespace removes a namespace.

operation.json
{
  "kind": "deleteNamespace",
  "data": { "id": "3c1d2e3f-0001-4a1a-9c1a-000000000001" }
}

Collections

A collection is a container for items of the same type. createCollection and updateCollection carry nested field and index operations in their own data.operations array.

Create a Collection

createCollection adds a collection and, optionally, its initial fields and indexes.

operation.json
{
  "kind": "createCollection",
  "data": {
    "id": "f4ccfb8e-5d79-4d2f-83b0-5b749f784b00",
    "sourceId": "d7c8f0e2-9b3a-4c1e-8f2a-1a2b3c4d5e6f",
    "namespaceId": null,
    "dbName": "articles",
    "apiName": "articles",
    "operations": []
  }
}
  • namespaceIdnull places the collection in the data source's default namespace. On the built-in system source, it places the collection in the project's _data namespace.
  • apiNamenull falls back to dbName
  • operations — nested createPrimitiveField and createIndex operations

Update a Collection

updateCollection applies nested field and index operations to an existing collection.

operation.json
{
  "kind": "updateCollection",
  "data": {
    "id": "f4ccfb8e-5d79-4d2f-83b0-5b749f784b00",
    "operations": []
  }
}

The nested operations array accepts createPrimitiveField, updatePrimitiveField, deletePrimitiveField, createIndex, and deleteIndex.

Rename a Collection

renameCollection changes a collection's dbName, apiName, or both. Set either to null to leave it unchanged.

operation.json
{
  "kind": "renameCollection",
  "data": {
    "id": "f4ccfb8e-5d79-4d2f-83b0-5b749f784b00",
    "dbName": "posts",
    "apiName": "posts"
  }
}

Delete a Collection

deleteCollection removes a collection.

operation.json
{
  "kind": "deleteCollection",
  "data": { "id": "f4ccfb8e-5d79-4d2f-83b0-5b749f784b00" }
}

Fields

A field is a named value on an item. createPrimitiveField, updatePrimitiveField, and deletePrimitiveField nest inside createCollection or updateCollection. renamePrimitiveField is top-level.

Create a Field

createPrimitiveField adds a field to a collection.

operation.json
{
  "kind": "createPrimitiveField",
  "data": {
    "id": "9568d51f-2dde-40c1-93e4-98cc3feaca53",
    "dbName": "title",
    "apiName": "title",
    "type": { "name": "string", "params": { "length": "unlimited" } },
    "isNullable": false,
    "isList": false,
    "defaultValue": null
  }
}
  • type — the field type object (see Field Types)
  • isListtrue stores an array of the type
  • defaultValue — a default value object, or null for no default

Update a Field

updatePrimitiveField applies one or more change operations to a field.

operation.json
{
  "kind": "updatePrimitiveField",
  "data": {
    "id": "9568d51f-2dde-40c1-93e4-98cc3feaca53",
    "operations": [
      { "kind": "changeIsNullable", "data": { "isNullable": true } }
    ]
  }
}

The nested operations array accepts:

KinddataChanges
changeType{ "type": { ... } }The field's type
changeIsNullable{ "isNullable": true }Whether the field accepts null
changeIsList{ "isList": true }Whether the field stores an array
changeDefaultValue{ "defaultValue": ... }The field's default value

Delete a Field

deletePrimitiveField removes a field from a collection.

operation.json
{
  "kind": "deletePrimitiveField",
  "data": { "id": "9568d51f-2dde-40c1-93e4-98cc3feaca53" }
}

Rename a Field

renamePrimitiveField changes a field's dbName, apiName, or both. This is a top-level operation. Set either to null to leave it unchanged.

operation.json
{
  "kind": "renamePrimitiveField",
  "data": {
    "id": "9568d51f-2dde-40c1-93e4-98cc3feaca53",
    "dbName": "headline",
    "apiName": "headline"
  }
}

Indexes

createIndex and deleteIndex nest inside createCollection or updateCollection.

Create an Index

createIndex adds an index over one or more fields.

operation.json
{
  "kind": "createIndex",
  "data": {
    "id": "d78c36f9-b627-433c-88bb-58eaadba50e1",
    "dbName": "articles_slug_idx",
    "fieldIds": ["9568d51f-2dde-40c1-93e4-98cc3feaca53"],
    "kind": "unique"
  }
}
  • fieldIds — the indexed fields, in order; at least one is required
  • kindunique, normal, or primary (primary backs a collection's primary key and makes every indexed field non-nullable)

Delete an Index

deleteIndex removes an index.

operation.json
{
  "kind": "deleteIndex",
  "data": { "id": "d78c36f9-b627-433c-88bb-58eaadba50e1" }
}

Relations

A relation links two collections. createRelationFieldPair creates both sides at once — one relation field on each collection. The unconstrained side's isList sets whether the relation is to-many or to-one.

Create a Relation

createRelationFieldPair creates a bidirectional relation. Its data is itself tagged with kind: "single".

operation.json
{
  "kind": "createRelationFieldPair",
  "data": {
    "kind": "single",
    "data": {
      "relationName": "article_author",
      "onUpdate": "noAction",
      "onDelete": "setNull",
      "firstField": {
        "id": "11111111-1111-4a1a-9c1a-aaaaaaaaaaaa",
        "apiName": "author",
        "isList": false,
        "isConstrained": true,
        "collectionId": "f4ccfb8e-5d79-4d2f-83b0-5b749f784b00",
        "fieldIds": ["d4e5f6a7-4444-4a1a-9c1a-555566667777"]
      },
      "secondField": {
        "id": "22222222-2222-4a1a-9c1a-bbbbbbbbbbbb",
        "apiName": "articles",
        "isList": true,
        "isConstrained": false,
        "collectionId": "a83bda5c-96bd-462f-8012-481115dfa751",
        "fieldIds": ["b8c9d0e1-6666-4a1a-9c1a-777788889999"]
      }
    }
  }
}

Each side is a relation field:

  • isConstrainedtrue on the side that holds the foreign key; set it to false on the other side
  • isList — set false on the constrained side; set it to true on the unconstrained side for a to-many relation, or false for a to-one relation
  • collectionId — the collection the field belongs to
  • fieldIds — the backing fields: the foreign key on the constrained side, the referenced key on the other

The fields in fieldIds are primitive fields that must already exist in each collection — create them before the relation references them. When a side lists more than one field, the two sides map positionally: the first field on the constrained side pairs with the first on the referenced side, and so on.

A relation that spans two data sources exists in the project model only. Monospace does not create or enforce a data source constraint for it, including its onUpdate and onDelete actions.

onUpdate and onDelete default to noAction when omitted. Both accept one of:

ActionBehavior
noActionMakes no automatic change. The data source performs its normal referential-integrity check.
cascadeCascades the delete or update through the relation.
restrictBlocks the operation while related items exist.
setNullSets the dependent backing fields to null. It fails when any backing field is non-nullable.
setDefaultSets the dependent backing fields to their defaults. It fails when a backing field has no default.

Update Relation Actions

updateRelationFieldPair changes one or both referential actions on an existing relation. Include both relation field IDs and at least one action.

operation.json
{
  "kind": "updateRelationFieldPair",
  "data": {
    "firstFieldId": "11111111-1111-4a1a-9c1a-aaaaaaaaaaaa",
    "secondFieldId": "22222222-2222-4a1a-9c1a-bbbbbbbbbbbb",
    "onDelete": "cascade"
  }
}

Omit an action to leave it unchanged. Set an action to null to reset it to noAction.

Delete a Relation

deleteRelationFieldPair removes both sides of a relation, identified by their field IDs.

operation.json
{
  "kind": "deleteRelationFieldPair",
  "data": {
    "firstFieldId": "11111111-1111-4a1a-9c1a-aaaaaaaaaaaa",
    "secondFieldId": "22222222-2222-4a1a-9c1a-bbbbbbbbbbbb"
  }
}

Rename a Relation Field

renameRelationField changes the apiName of one side of a relation.

operation.json
{
  "kind": "renameRelationField",
  "data": {
    "id": "11111111-1111-4a1a-9c1a-aaaaaaaaaaaa",
    "apiName": "writer"
  }
}

Field Types

The type object tags a primitive type by name. Most types need only a name. string and decimal carry a params object.

type.json
{ "name": "boolean" }
{ "name": "string", "params": { "length": "unlimited" } }
{ "name": "string", "params": { "length": "fixed", "value": 255 } }
{ "name": "decimal", "params": { "precision": 10, "scale": 2 } }
CategoryNames
Integerint8, int16, int32, int64, uint8, uint16, uint32, uint64
Floating pointfloat32, float64, decimal
Textstring, uuid
Booleanboolean
Temporaldate, time, dateTime, dateTimeWithTimezone
Otherjson, bytes, geometry
Unsigned integer types (uint8uint64) and geometry are part of the format but cannot currently be applied to a data source — a migration that uses them fails. Introspection may also report a field as unsupported when Monospace does not model its type; you cannot author that type.

Default Values

A field's defaultValue is null for no default, or an object tagged by source. Two sources exist.

Engine-Managed Defaults

An engine-managed default resolves a value whenever the field is omitted from a write. It fills the gap — it never overrides a value the caller provides.

Defaults resolve per write phase. onCreate and onUpdate are independent and each optional.

FieldApplies when
onCreateThe field is omitted while creating an item
onUpdateThe field is omitted while updating an item

Set only the phase you need. onCreate alone applies a value on create and leaves updates untouched; onUpdate alone applies a value on update but not on create. An engineManaged default must set at least one of the two — for no default at all, use defaultValue: null.

Each phase holds a source object tagged by kind. Both phases resolve against the field's single type, so keep them consistent — a static boolean on create paired with a currentTimestamp on update describes two different types for one field.

kindFieldsResolves to
staticvalueEncodedA literal value, encoded as JSON
currentTimestampThe write time (now())
currentUserThe authenticated user's key
randomUuidversion (v4 or v7)A newly generated UUID
Defaults are not supported on list fields. A static value must be compatible with the field's type. currentTimestamp requires a date, time, dateTime, or dateTimeWithTimezone field. currentUser and randomUuid require a uuid field.

valueEncoded does not preserve the value's type. Monospace coerces it using the field's type while applying the write, so 2 supplied for a string field becomes "2".

Set onCreate alone for a value fixed at insert time.

A status field that starts at "draft":

defaultValue.json
{ "source": "engineManaged", "onCreate": { "kind": "static", "valueEncoded": "draft" } }

A created_at field stamped once, when the item is created:

defaultValue.json
{ "source": "engineManaged", "onCreate": { "kind": "currentTimestamp" } }

A created_by field set to whoever creates the item:

defaultValue.json
{ "source": "engineManaged", "onCreate": { "kind": "currentUser" } }

An id field that receives a UUID v7 when the item is created:

defaultValue.json
{ "source": "engineManaged", "onCreate": { "kind": "randomUuid", "version": "v7" } }

Set both phases to keep a field current on every write — this is what the phase split buys you.

An updated_at field stamped on create and refreshed on each later update:

defaultValue.json
{
  "source": "engineManaged",
  "onCreate": { "kind": "currentTimestamp" },
  "onUpdate": { "kind": "currentTimestamp" }
}

An updated_by field set to the creator, then to whoever updates the item next:

defaultValue.json
{
  "source": "engineManaged",
  "onCreate": { "kind": "currentUser" },
  "onUpdate": { "kind": "currentUser" }
}

Connector-Managed Defaults

The data source owns the value, as with autoincrement, serial, or identity fields. Introspection reports these; you rarely author them.

defaultValue.json
{ "source": "connectorManaged", "isAutoincrement": true }

isAutoincrement is true for autoincrement fields, or null when the connector does not report it.

See Also

Copyright © 2026