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.
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
| Column | Postgres | TypeScript |
|---|---|---|
t.text() | text | string |
t.integer() | bigint | number |
t.number() | double precision | number |
t.boolean() | boolean | boolean |
t.timestamp() | timestamptz | string, in ISO 8601 |
t.uuid() | uuid | string |
t.json() | jsonb | any JSON value |
Every column is required unless you mark it:
.optional()lets the column benull. Its type in TypeScript becomesT | 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 pushpush 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 once | Destructive: held on main |
|---|---|
create_table, add_column, change_default, allow_empty (make a column optional), create_index, drop_index, create_folder, change_folder | drop_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.