TitanbaseDocs

.titan.json format

A .titan.json file is the Titanbase schema — a portable JSON document shared by the editor and the exporters. This page is the canonical reference, verified against the TitanSchema type and Zod schemas in @titanbase/core.

The current format version is 1.0 (titanVersion: "1.0").

File convention#

  • Extension: .titan.json
  • Encoding: UTF-8
  • Top level: a single JSON object
  • Saved with 2-space indentation

Note

There is one concept worth keeping in mind: semantic schema vs editor metadata. Tables, columns, relations, and enums describe what the database looks like. Editor metadata (metadata.editor) only describes how the schema is laid out on the canvas. Exporters never read editor metadata.

Root object#

{
  "titanVersion": "1.0",
  "project": { "id": "blog", "name": "Blog", "description": "Optional description" },
  "dialect": "postgres",
  "tables": [],
  "enums": [],
  "relations": [],
  "metadata": { "editor": { "tablePositions": {} } }
}
FieldTypeRequiredDescription
titanVersion"1.0"YesFormat version. Must be the literal "1.0".
projectobjectYesProject identity (see below).
dialectenumYespostgres, mysql, sqlite, or generic.
tablesTable[]YesTable definitions (may be empty []).
enumsEnum[]YesShared enum definitions (may be empty []).
relationsRelation[]YesForeign-key relations (may be empty []).
metadataobjectYesEditor metadata (see below).

There is no $schema field, no root-level name, and no targetHints in the current format.

project#

{ "id": "blog", "name": "Blog", "description": "A simple blog schema" }
FieldTypeRequiredDescription
idstringYesProject identifier.
namestringYesHuman-readable project name.
descriptionstringNoOptional description.

dialect#

The dialect tells diagnostics which column types to recognize and which warnings to raise (for example, partial indexes are flagged outside PostgreSQL). The exporters available today target PostgreSQL (and PostgreSQL-flavored Prisma/Drizzle); see the type system and Exporters.

Tables#

{
  "id": "t_users",
  "name": "users",
  "schema": "public",
  "description": "Application users",
  "columns": [],
  "indexes": []
}
FieldTypeRequiredDescription
idstringYesStable table identifier (referenced by relations, indexes, and positions).
namestringYesTable name.
schemastringNoOptional namespace (e.g. public).
descriptionstringNoHuman-readable description.
columnsColumn[]YesColumn definitions.
indexesIndex[]YesIndex definitions (may be empty []).

Both tables and columns have an id. Ids are how relations, indexes, and editor positions refer to objects, so they stay stable even if you rename a table or column. There is no checks field in the current format.

Columns#

{
  "id": "c_users_email",
  "name": "email",
  "type": "text",
  "nullable": false,
  "primaryKey": false,
  "unique": true,
  "default": "'unknown'",
  "description": "User email address"
}
FieldTypeRequiredDescription
idstringYesStable column identifier.
namestringYesColumn name.
typestringYesColumn type — a built-in type or an enum name (see type system).
nativeTypestringNoAn exact target type to emit instead of mapping type.
nullablebooleanYesWhether NULL is allowed.
primaryKeybooleanYesWhether this column is part of the primary key.
uniquebooleanYesWhether a unique constraint applies.
defaultstringNoDefault value as a string (see below).
descriptionstringNoHuman-readable description.

nullable, primaryKey, and unique are required booleans — set them explicitly. The format has no maxLength, precision, scale, values, enumRef, or arrayOf fields. To pin an exact database type, set nativeType (for example "nativeType": "varchar(255)").

Default values#

default is a string, not an object. It is written the way it would appear in SQL:

{ "default": "'active'" }              // string literal (quoted)
{ "default": "0" }                      // numeric literal
{ "default": "true" }                   // boolean literal
{ "default": "now()" }                  // function call
{ "default": "gen_random_uuid()" }      // function call

Diagnostics check that a default looks compatible with the column type, and exporters translate well-known functions (now(), gen_random_uuid()) into the equivalent for each target.

Enums#

Shared enum definitions live at the root and are referenced by a column's type:

{
  "enums": [
    { "id": "e_status", "name": "status", "values": ["draft", "published", "archived"] }
  ]
}
FieldTypeRequiredDescription
idstringYesStable enum identifier.
namestringYesEnum name. Used as a column type to reference it.
valuesstring[]YesAt least one value.
descriptionstringNoHuman-readable description.

To use an enum, set a column's type to the enum's name:

{ "id": "c_posts_status", "name": "status", "type": "status", "nullable": false, "primaryKey": false, "unique": false }

There is no enumRef field — the reference is the type string matching an enum name.

Relations#

Relations are stored at the root (not inside tables). Endpoints are from (the foreign-key side) and to (the referenced side), and each endpoint names a table id and one or more column ids — so composite keys are supported.

{
  "id": "r_posts_author",
  "name": "posts_author_id_fkey",
  "from": { "table": "t_posts", "columns": ["c_posts_author_id"] },
  "to": { "table": "t_users", "columns": ["c_users_id"] },
  "cardinality": "many-to-one",
  "onDelete": "cascade",
  "onUpdate": "no-action"
}
FieldTypeRequiredDescription
idstringYesStable relation identifier.
namestringYesRelation / constraint name.
fromendpointYes{ table, columns[] } — the foreign-key side.
toendpointYes{ table, columns[] } — the referenced side.
cardinalityenumYesone-to-one, one-to-many, many-to-one, many-to-many.
onDeleteenumNoReferential action (see below).
onUpdateenumNoReferential action (see below).

Endpoint columns reference column ids, and table references a table id. The two endpoints must have the same number of columns.

Referential actions: cascade, restrict, set-null, set-default, no-action.

Note

The format does not include a join-table field for many-to-many relations. For portable output, model a many-to-many relationship with an explicit join table and two many-to-one relations; diagnostics will warn on a bare many-to-many.

Indexes#

Indexes are stored inside their table's indexes array, and each index also records the table id it belongs to. Index columns reference column ids (order matters).

{
  "id": "i_users_email",
  "name": "idx_users_email",
  "table": "t_users",
  "columns": ["c_users_email"],
  "unique": true,
  "method": "btree",
  "where": "deleted_at IS NULL",
  "description": "Active users by email"
}
FieldTypeRequiredDescription
idstringYesStable index identifier.
namestringYesIndex name.
tablestringYesOwning table id (must match the table).
columnsstring[]YesColumn ids (at least one, order matters).
uniquebooleanYesWhether the index is unique.
methodstringNoIndex method (e.g. btree, gin).
wherestringNoPartial-index predicate (PostgreSQL).
descriptionstringNoHuman-readable description.

A where predicate is PostgreSQL-specific; diagnostics warn when the dialect is not PostgreSQL.

Editor metadata#

metadata.editor.tablePositions stores canvas positions, keyed by table id. This is editor-only data; exporters ignore it.

{
  "metadata": {
    "editor": {
      "tablePositions": {
        "t_users": { "x": 100, "y": 120 },
        "t_posts": { "x": 460, "y": 120 }
      }
    }
  }
}
FieldTypeRequiredDescription
editorobjectYesEditor metadata container.
editor.tablePositionsrecordYesMap of table id → { x, y } (finite numbers).

Positions are keyed by table id, not table name, so renaming a table keeps its layout.

Examples#

Minimal valid schema#

{
  "titanVersion": "1.0",
  "project": { "id": "minimal", "name": "Minimal" },
  "dialect": "postgres",
  "tables": [
    {
      "id": "t_users",
      "name": "users",
      "columns": [
        { "id": "c_users_id", "name": "id", "type": "uuid", "nullable": false, "primaryKey": true, "unique": false }
      ],
      "indexes": []
    }
  ],
  "enums": [],
  "relations": [],
  "metadata": { "editor": { "tablePositions": { "t_users": { "x": 0, "y": 0 } } } }
}
{
  "titanVersion": "1.0",
  "project": { "id": "blog", "name": "Blog" },
  "dialect": "postgres",
  "tables": [
    {
      "id": "t_users",
      "name": "users",
      "columns": [
        { "id": "c_users_id", "name": "id", "type": "uuid", "nullable": false, "primaryKey": true, "unique": false, "default": "gen_random_uuid()" },
        { "id": "c_users_email", "name": "email", "type": "text", "nullable": false, "primaryKey": false, "unique": true }
      ],
      "indexes": []
    },
    {
      "id": "t_posts",
      "name": "posts",
      "columns": [
        { "id": "c_posts_id", "name": "id", "type": "uuid", "nullable": false, "primaryKey": true, "unique": false, "default": "gen_random_uuid()" },
        { "id": "c_posts_author_id", "name": "author_id", "type": "uuid", "nullable": false, "primaryKey": false, "unique": false },
        { "id": "c_posts_title", "name": "title", "type": "text", "nullable": false, "primaryKey": false, "unique": false }
      ],
      "indexes": [
        { "id": "i_posts_author", "name": "idx_posts_author_id", "table": "t_posts", "columns": ["c_posts_author_id"], "unique": false }
      ]
    }
  ],
  "enums": [],
  "relations": [
    {
      "id": "r_posts_author",
      "name": "posts_author_id_fkey",
      "from": { "table": "t_posts", "columns": ["c_posts_author_id"] },
      "to": { "table": "t_users", "columns": ["c_users_id"] },
      "cardinality": "many-to-one",
      "onDelete": "cascade"
    }
  ],
  "metadata": {
    "editor": {
      "tablePositions": {
        "t_users": { "x": 80, "y": 80 },
        "t_posts": { "x": 460, "y": 80 }
      }
    }
  }
}

Enum#

{
  "enums": [
    { "id": "e_status", "name": "post_status", "values": ["draft", "published", "archived"] }
  ],
  "tables": [
    {
      "id": "t_posts",
      "name": "posts",
      "columns": [
        { "id": "c_posts_id", "name": "id", "type": "uuid", "nullable": false, "primaryKey": true, "unique": false },
        { "id": "c_posts_status", "name": "status", "type": "post_status", "nullable": false, "primaryKey": false, "unique": false, "default": "'draft'" }
      ],
      "indexes": []
    }
  ]
}

Index#

{
  "id": "i_users_email",
  "name": "uq_users_email",
  "table": "t_users",
  "columns": ["c_users_email"],
  "unique": true
}

Serialization and versioning#

  • Titan JSON export is produced with JSON.stringify(schema, null, 2) — readable, 2-space-indented JSON.
  • On load, @titanbase/core normalizes a schema: it trims string fields and sorts the tables, enums, and relations arrays by name. This keeps related schemas consistent, though object key order is not alphabetized, so do not assume byte-identical output across tools.
  • The format is versioned by titanVersion. The current version is "1.0"; a file with any other value is rejected with a clear diagnostic.

JSON Schema#

Status: Planned

A published JSON Schema file for static validation is planned but not available yet. Today, validation is performed in code by @titanbase/core (Zod schemas plus diagnostics). See Diagnostics.