MantisDocs

Schema

Tables, columns, defaults and indexes in TypeScript, and what happens when you push a change.

The schema lives in one file, mantis/schema.ts by default. It is the source of truth: you or your agent edit it, and push (or the apply_migration tool) makes the database match.

mantis/schema.ts
import { defineSchema, t, table } from "@shipmantis/sdk";

export default defineSchema({
  todos: table({
    title: t.text(),
    done: t.boolean().default(false),
    dueAt: t.timestamp().optional(),
    listId: t.uuid(),
  }).index("listId", "createdAt"),

  lists: table({
    name: t.text(),
    settings: t.json().default({}),
  }),
});

push loads the file's default export. A named schema export works too.

Columns

ColumnPostgresTypeScript
t.text()textstring
t.integer()bigintnumber
t.number()double precisionnumber
t.boolean()booleanboolean
t.timestamp()timestamptzstring, in ISO 8601
t.uuid()uuidstring
t.json()jsonbany JSON value

Every column is required unless you mark it:

  • .optional() lets the column be null. Its type in TypeScript becomes T | null.
  • .default(value) fills the column when an insert leaves it out. A timestamp takes "now" and a uuid takes "random", as well as a fixed value.

A column with either one can be left out of an insert; the types follow.

Every table also has two columns you don't declare: id, a random uuid, and createdAt, the time the row was inserted. Both are filled for you and both can be used in queries and indexes.

Indexes

table({ listId: t.uuid(), done: t.boolean() })
  .index("listId")
  .index("listId", "done", "createdAt");

Each .index(...) call adds one index over the columns you name, in that order. Add one for the columns you filter or sort by most.

Pushing a change

npx shipmantis push

push sends the schema file to Mantis, which compares it with the branch and plans the steps between them. Every step is marked safe or destructive:

Safe: applies at onceDestructive: held on main
create_table, add_column, change_default, allow_empty (make a column optional), create_index, drop_index, create_folder, change_folderdrop_table, drop_column, change_type, require_column (make a column required), change_rule and change_file_rule when they let more people in, change_auth, drop_folder

A destructive step on main is not applied. The whole change is held until a person on the team approves it in the dashboard, and push exits with code 2:

1 change:
  drop_column    todos.dueAt  (can lose data)
These changes can lose data, so they wait on main until a person approves them in Mantis.
Change 6f1c… is waiting for approval.

On any other branch every step applies at once. That is where the agent should try a destructive change first.

Run push --dry-run to see the plan without applying it. Your agent does the same with the plan_migration tool, which also shows the SQL for each step.

Adding a required column

A required column added to an existing table needs a .default(...), so Mantis can fill the rows already there. Without one the plan is refused: add a default, or make it .optional().

Undo

Each push that changes something is one migration with a version number. npx shipmantis undo reverses the latest one, bringing back a dropped table or column with its data. See Rewind.

On this page