Skip to content

Data Objects

A data object is Ion Drive’s core primitive: a table you define at runtime, plus the metadata that makes it self-describing. Creating one instantly lights up REST, GraphQL, and MCP endpoints for it — no migrations, no redeploys.

Ion Drive keeps its own metadata separate from your data:

  • User data objects — the tables you create (contacts, invoices, …).
  • System tables — Ion Drive’s own metadata, prefixed _ion_ (_ion_objects, _ion_fields, _ion_relationships, _ion_migrations, and the auth/secrets/tasks/blocks tables). These are hidden from the data API.

Don’t confuse the two: system tables describe the platform; data objects hold your application’s data.

{
"name": "contacts", // lowercase, snake_case — the API path segment
"displayName": "Contacts", // human label
"description": "People we work with",
"fields": [
{ "name": "full_name", "displayName": "Full Name", "columnType": "text", "isRequired": true },
{ "name": "email", "displayName": "Email", "columnType": "email", "isUnique": true },
{ "name": "status", "displayName": "Status", "columnType": "enum",
"constraints": { "enumValues": ["active", "archived", "pending"] }, "defaultValue": "active" }
]
}

Every object automatically gets three system fields: id (UUID primary key), created_at, and updated_at. You never define these yourself.

OptionMeaning
columnTypeOne of the built-in column types.
isRequiredNOT NULL.
isUniqueAdds a unique constraint.
isIndexedCreates an index (also automatic for FKs and unique fields).
defaultValueSQL default (literals are quoted for you; expressions pass through).
constraintsmin, max, pattern, enumValues, message.

Composite unique constraints (uniqueTogether)

Section titled “Composite unique constraints (uniqueTogether)”

Uniqueness across multiple fields together lives on the object, not any single field:

{
"name": "matches",
"displayName": "Matches",
"fields": [
{ "name": "room_code", "displayName": "Room", "columnType": "text" },
{ "name": "seed", "displayName": "Seed", "columnType": "integer" }
],
"constraints": { "uniqueTogether": [["room_code", "seed"]] }
}

Each group (two or more field names) becomes a real, named PostgreSQL constraint — ion_uq_<table>_<col1>_<col2> — so the database itself enforces it (ADR-017 rule 1: metadata mirrors DB truth). Groups are stored normalized (columns sorted, groups sorted), round-trip through the schema snapshot (ion-drive schema pull/diff/push), and are valid upsert conflict targets.

Manage them after creation with PATCH /api/v1/schema/objects/:name (body { "constraints": { "uniqueTogether": [...] } }) or the MCP set_unique_together tool. The call is declarative — the supplied groups become the full set; untouched groups never churn — and preview-first: ?dryRun=true returns the exact DDL, and adding a group pre-checks existing rows so duplicate combinations fail with named samples instead of a raw database error. Single-field uniqueness stays on the field (isUnique).

Column types are grouped by category. Text-category types (and enums) are what free-text search matches against.

CategoryTypes
Texttext, short_text, long_text, rich_text, email, url, phone, slug
Numberinteger, big_integer, decimal, float, percentage, currency
Booleanboolean
Date/timedate, datetime, time
Identityuuid, auto_increment
Structuredjson, array_text, array_integer
Enumenum (single select), multi_enum (multi select)
Specialrating, color, ip_address

Get the live list any time via the MCP list_column_types tool or the COLUMN_TYPES export from @ion-drive/core.

json fields accept any JSON value on write — send the object or array itself in the request body (no pre-encoding), and reads return the same parsed value. A pre-encoded JSON string is also accepted for compatibility.

Objects can relate via one_to_one, one_to_many, many_to_one, and many_to_many (junction tables are created automatically). Related records are readable from both sides via relation keys — expand= on REST/MCP and nested fields on GraphQL (see the querying guide); m2m links are written through the link endpoints/mutations (Phase 13).

Relationships can also be removed (Phase 13): the same preview-first contract as field changes — DELETE /api/v1/schema/objects/:name/relationships/:relName?dryRun=true returns the exact SQL and data-loss warnings (the FK column’s stored links, or the junction table’s counted rows), block-owned relationships require ?force=true, and ion-drive schema push --prune removes relationships absent from the snapshot through the same pipeline. There is deliberately no automated migration rollback (_ion_migrations.sql_down is advisory documentation; ADR-020) — recovery is declarative (snapshot pull/diff/push) plus database backups.

Every schema mutation runs the same pipeline:

  1. Build a ChangeSet describing the operation.
  2. Validate it — the ChangeValidator/impact-analyzer flags data loss, broken foreign keys, and constraint violations.
  3. Preview — a human-readable diff and the exact SQL (also available via the admin console and the MCP ion_schema_preview capability).
  4. Execute the DDL transactionally, record metadata, and bump the registry version so all API surfaces refresh.

Destructive changes (dropping a column or object with data) produce warnings you must acknowledge.

  • Admin console: the visual Object Designer.
  • REST: POST /api/v1/schema/objects, and the schema routes under /api/v1/schema.
  • MCP: create_object, add_field, delete_object, get_object.
  • Building blocks: ship a whole set of objects at once — see Building Blocks.