Skip to content

Schema changes

Included free on every install. The schema canvas and saving presets of your own need a license with the schema-pro feature, and moving configuration between instances needs config-sync. See pricing.

A content type changes while your site keeps serving it. You send the new definition, and LyEve changes only what differs, so existing entries keep their values. Before anything runs, you can see the exact database statements a change would make. A removal never runs by accident: the data stays until a super admin applies it.

What a content type holds, its fields, field types and relations, is explained on the data model page.

Each save compares the new definition with the stored one and acts on the difference:

You changeWhat happens
Add a fieldApplied at once. Existing entries have no value for it.
Turn required, unique or indexed on or offApplied at once.
Change a field's typeApplied at once, and the stored values are converted. On PostgreSQL a value that does not convert, such as abc into a number, fails the save with 422 schema migration failed, and nothing changes.
Remove a field, or turn off with_soft_delete or with_draft_publishQueued. The data stays until a super admin applies it.
Rename a field or the content typeUse the rename routes below. A definition that just uses the new name is read as one field added and one removed.

This adds a field to a content type, removes another, and applies the removal. You need a token in TOKEN for a super_admin. The quickstart shows how to get one.

  1. Create a note content type with a color field:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "note", "display_name": "Note", "fields": [{"name": "title", "field_type": "text", "required": true}, {"name": "color", "field_type": "text"}]}'

    The answer is 200 with the stored definition, which now starts with the id field every content type gets.

  2. Preview a change that adds pinned and drops color. Nothing is changed yet:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas/note/preview-ddl \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"display_name": "Note", "fields": [{"name": "title", "field_type": "text", "required": true}, {"name": "pinned", "field_type": "boolean"}]}'
    {
    "statements": [
    {
    "description": "add column _note.pinned",
    "sql": "ALTER TABLE \"_note\" ADD COLUMN IF NOT EXISTS \"pinned\" BOOLEAN",
    "down_sql": "ALTER TABLE \"_note\" DROP COLUMN IF EXISTS \"pinned\""
    },
    {
    "description": "drop column _note.color",
    "sql": "ALTER TABLE \"_note\" DROP COLUMN IF EXISTS \"color\"",
    "down_sql": "-- no safe undo for drop of _note.color",
    "destructive": true
    }
    ]
    }

    "destructive": true marks a statement the save queues instead of running. The SQL is written for your database, PostgreSQL here.

  3. Save the change by sending the same definition with its name:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "note", "display_name": "Note", "fields": [{"name": "title", "field_type": "text", "required": true}, {"name": "pinned", "field_type": "boolean"}]}'

    pinned exists now. color and its values are still stored.

  4. See what is queued:

    Terminal window
    curl http://localhost:3001/api/admin/schemas/note/history \
    -H "Authorization: Bearer $TOKEN"

    The answer lists each statement with its state (applied, pending or failed), and "pending": 1.

  5. Apply the removal:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas/note/apply-pending \
    -H "Authorization: Bearer $TOKEN"
    { "applied": 1, "canceled": 0 }
  6. In the admin console, open Schema builder and pick Note. Preview DDL shows the statements for unsaved edits, and History lists what ran and what is queued.

Two routes answer the statements a definition would run, and change nothing:

  • POST /api/admin/schemas/{name}/preview-ddl marks each queued statement with "destructive": true. The Schema builder reads this one.
  • POST /api/admin/schemas/{name}/preview answers the same statements without the mark.

Each statement has a description, its sql, and a down_sql that undoes it where that is possible. The name in the path wins over a name in the body.

POST /api/admin/schemas creates a content type or updates the one with the same name. It takes admin or super_admin.

PUT /api/admin/schemas/{name} replaces the definition of a content type that exists and answers 404 for one that does not. It takes super_admin. A definition it refuses answers 400 with the reason, where POST answers 422.

Both answer 200 with the stored definition, and every save is recorded in the audit log.

Terminal window
curl -X PUT http://localhost:3001/api/admin/schemas/note/fields/pinned/rename \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"new_name": "starred"}'
{ "schema": "note", "old_field": "pinned", "new_field": "starred" }

PUT /api/admin/schemas/{name}/rename with {"new_name": "memo"} renames the content type and answers its stored definition. Its Content API path moves to the new name, and the entries stored there move with it. A name another content type already uses answers 409.

A removal waits in a queue until a super admin applies it, because it changes stored data for every tenant that defines a content type of that name. A queued removal whose field a definition uses again is canceled instead of run.

ToCall
See one content type's statements and what is queuedGET /api/admin/schemas/{name}/history
Apply one content type's queuePOST /api/admin/schemas/{name}/apply-pending
See every queued statement, by content typeGET /api/admin/migrate
Apply every queuePOST /api/admin/migrate/apply, or POST /api/admin/schemas/migrate, which also answers a message

In the console, a super admin applies a queue from History in the Schema builder.

DELETE /api/admin/schemas/{name} removes the definition and your tenant's entries, and answers 204. Add ?content=soft to keep the entries the admin console wrote (their title, slug and body) marked deleted instead, so a content type you create again under the same name can take them back. The rows the Content API serves go either way. Deleting a content type that does not exist also answers 204.

The Schema builder has two views, List and Canvas. The list editor is free on every install and builds every field type and every relation. The canvas needs a license with the schema-pro feature. It draws each content type as a box and each relation as a line between two boxes, so you see the whole model at once.

On the canvas you can:

  • Edit a content type. Pick its box, and the same editor the list view shows opens beside the diagram.
  • Add a field. Start it from the box it belongs to.
  • Add a relation. Draw it from one box to the content type it points at.
  • Arrange the diagram. Drag the boxes where you want them. The layout saves a moment after you stop, and every admin of the tenant opens the same diagram, after a reload too.

Without schema-pro, choosing Canvas shows what the canvas does and that it comes with Schema Pro, and the list editor works as before. If a license lapses, the saved layout stays. The canvas opens in the same arrangement once the license is back.

Over the Admin API, PUT /api/admin/schemas/canvas-layout replaces the saved layout, up to 256 KiB, and answers 402 without schema-pro:

{ "positions": { "article": { "x": 120, "y": 40 }, "author": { "x": 420, "y": 40 } } }

GET on the same path reads the saved layout on any install. Its answer also carries licensed, which says whether this install has schema-pro. schema-pro is in beta, so the layout's fields can still change before it is marked stable.

With a license that carries config-sync, a super admin exports this instance's content types, flows, access rules, webhooks and a few settings as one bundle sealed with a passphrase, compares it with another instance, dry runs it and applies it there. See Promote configuration. Moving one content type at a time stays free, as described in Keep the content model and flows in Git.

PUT /api/admin/schemas/{name} and POST /api/admin/schemas/migrate share a budget of ten calls per tenant in any five minutes. Past it the answer is 429 with a Retry-After header:

{ "error": "schema mutation rate limit exceeded", "retry_after": 120 }

When two replicas apply a change to the same content type at once, one answers 503 with Retry-After: 1. Send the request again.

Schema changes have no settings of their own. They run on every install. If you set LYEVE_PLUGINS to choose which features start, include schema in it. See licensing and tiers.

GET /api/admin/schemas, GET /api/admin/schemas/{name} and its stats also accept an admin token with the schemas:read grant. Every other route takes a signed-in session.

Content type routes
MethodPathRolePurpose
GET/api/admin/schemassigned inList content types.
GET/api/admin/schemas/{name}signed inOne content type.
GET/api/admin/schemas/{name}/statssigned inYour tenant's entry count, as rows, and last_updated.
POST/api/admin/schemasadminCreate or update a content type.
PUT/api/admin/schemas/{name}super_adminReplace a content type's definition.
DELETE/api/admin/schemas/{name}adminDelete a content type and its entries. ?content=soft keeps the entries.
PUT/api/admin/schemas/{name}/renameadminRename a content type. Body: new_name.
PUT/api/admin/schemas/{name}/fields/{field}/renameadminRename a field and keep its data. Body: new_name.
POST/api/admin/schemas/{name}/preview-ddladminThe statements a definition would run, with queued ones marked.
POST/api/admin/schemas/{name}/previewadminThe same statements, unmarked.
GET/api/admin/schemas/{name}/historysuper_adminThe statements recorded for a content type, newest first, and how many are pending.
POST/api/admin/schemas/{name}/apply-pendingsuper_adminApply one content type's queue.
GET/api/admin/migratesuper_adminEvery queued statement, by content type.
POST/api/admin/migrate/applysuper_adminApply every queue.
POST/api/admin/schemas/migratesuper_adminApply every queue and report the count.
GET, PUT/api/admin/schemas/canvas-layoutadminRead or save the canvas layout. Saving needs schema-pro.
POST/api/admin/config-sync/export, .../diff, .../applysuper_adminExport, compare or apply a configuration bundle. Needs config-sync. See Promote configuration.
GET/api/v1/schemas, /api/v1/schemas/{name}signed inRead content types from the Content API.

The two you meet most:

  • 422 with the reason, such as an invalid name or an unknown field_type, when POST /api/admin/schemas refuses a definition. Nothing changes.
  • 429 schema mutation rate limit exceeded past ten changes in five minutes.
Every error these routes return
StatusMessageCause
400invalid JSONThe body is not JSON.
400The reason the definition was refusedPUT with an invalid field or option.
402payment_requiredSaving the canvas layout needs schema-pro, or a configuration bundle route needs config-sync.
404schema not found or field not found in schemaUnknown name.
409a schema with that name already existsA rename onto a name in use.
409another schema shares this table and defines it differently: ...Another tenant's content type of the same name stores the field as another type, or with a name that differs only in case.
413layout too largeThe canvas layout is over 256 KiB.
422invalid schema: ...POST with an invalid name, field or option.
422schema migration failedThe database refused the change, such as stored values that do not convert to a new field type. Nothing changes.
422new_name is required or new_name must differ from current nameBad rename body.
422content must be hard or softBad content on delete.
429schema mutation rate limit exceededOver ten changes in five minutes.
503another instance is applying DDL for this schema; retry in 1sAnother replica is changing the same content type. Retry.
503A static messageThe database could not be reached.