.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": {} } }
}
| Field | Type | Required | Description |
|---|---|---|---|
titanVersion | "1.0" | Yes | Format version. Must be the literal "1.0". |
project | object | Yes | Project identity (see below). |
dialect | enum | Yes | postgres, mysql, sqlite, or generic. |
tables | Table[] | Yes | Table definitions (may be empty []). |
enums | Enum[] | Yes | Shared enum definitions (may be empty []). |
relations | Relation[] | Yes | Foreign-key relations (may be empty []). |
metadata | object | Yes | Editor 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" }
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Project identifier. |
name | string | Yes | Human-readable project name. |
description | string | No | Optional 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": []
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable table identifier (referenced by relations, indexes, and positions). |
name | string | Yes | Table name. |
schema | string | No | Optional namespace (e.g. public). |
description | string | No | Human-readable description. |
columns | Column[] | Yes | Column definitions. |
indexes | Index[] | Yes | Index 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable column identifier. |
name | string | Yes | Column name. |
type | string | Yes | Column type — a built-in type or an enum name (see type system). |
nativeType | string | No | An exact target type to emit instead of mapping type. |
nullable | boolean | Yes | Whether NULL is allowed. |
primaryKey | boolean | Yes | Whether this column is part of the primary key. |
unique | boolean | Yes | Whether a unique constraint applies. |
default | string | No | Default value as a string (see below). |
description | string | No | Human-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"] }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable enum identifier. |
name | string | Yes | Enum name. Used as a column type to reference it. |
values | string[] | Yes | At least one value. |
description | string | No | Human-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"
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable relation identifier. |
name | string | Yes | Relation / constraint name. |
from | endpoint | Yes | { table, columns[] } — the foreign-key side. |
to | endpoint | Yes | { table, columns[] } — the referenced side. |
cardinality | enum | Yes | one-to-one, one-to-many, many-to-one, many-to-many. |
onDelete | enum | No | Referential action (see below). |
onUpdate | enum | No | Referential 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable index identifier. |
name | string | Yes | Index name. |
table | string | Yes | Owning table id (must match the table). |
columns | string[] | Yes | Column ids (at least one, order matters). |
unique | boolean | Yes | Whether the index is unique. |
method | string | No | Index method (e.g. btree, gin). |
where | string | No | Partial-index predicate (PostgreSQL). |
description | string | No | Human-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 }
}
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
editor | object | Yes | Editor metadata container. |
editor.tablePositions | record | Yes | Map 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 } } } }
}
Two related tables#
{
"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/corenormalizes a schema: it trims string fields and sorts thetables,enums, andrelationsarrays 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: PlannedA 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.