Chitragupt is the ORM and migration engine — the orm chitragupt line in the project manifest selects it. Its defining idea is that you never write a migration. You describe the schema you want — implicitly, by authoring entities — and Chitragupt figures out the SQL to get the database there. It is a desired-state engine (the Atlas/Terraform model), not a replay-a-fixed-log one.
The desired state comes from your entities
You already authored the schema without knowing it. Every entity, its fields, its relationships, its indexes, and its persistence-layer invariants are the desired schema. When you build the project, that intent is compiled into one artifact — a machine-readable desired schema (gen/schema.json) — the single source of truth for what the database should look like.
Take Chowk's OrderLine:
entity OrderLine aggregate_root global {
audit timestamps
indexes [
{ name "uk_order_line_id" kind unique fields [id] }
{ name "ix_order_line_order" kind standard fields [order_id] }
]
id uuid pk filterable[eq]
order_id uuid required immutable filterable[eq] relates Order many_to_one on_delete cascade
product_id uuid required immutable filterable[eq]
quantity int32 required
unit_price Money required
created_at timestamp immutable sortable
updated_at timestamp
}From this alone, Chitragupt knows the table it wants: an order_line table with typed columns, a primary key on id, a foreign key order_id → order(id) that is ON DELETE CASCADE (from on_delete cascade), a unique index and a standard index, and the audit timestamp columns. You wrote no DDL — the table is derived.
generate diffs desired vs. actual; migrate applies the delta
Two commands carry the model:
chitragupt generateconnects to the live database, reads its actual schema, compares it against the desired schema (gen/schema.json), and writes a migration containing only the difference. If the database already matches, the diff is empty — "Schema is up to date. No migration needed." — and nothing is written. This is what makes it idempotent: re-running against an up-to-date database is a no-op.chitragupt migrateapplies the pending migration to the database.
Because the migration is computed from the current state of the target, the same desired schema produces different SQL against different databases — a fresh database gets CREATE TABLE; an existing one missing a single column gets just ALTER TABLE … ADD COLUMN. You never think about "which migration number am I on"; you think about "what should the schema be," and the delta to get there is derived.
This is why migrations aren't committed. The repo's source of truth is the desired schema (
gen/schema.json), authored via entities. The SQL is derived at deploy time against whatever the database currently is. There is no hand-maintainedmigrations/*.sqllog to drift from the model.
What the diff covers
Chitragupt's diff manages the full shape of a relational schema, not just tables:
| Object | Derived from |
|---|---|
| Schemas (one per sub-domain) | the sub-domain |
| Tables & typed columns | the entity and its fields |
| Nullability, defaults, immutability | field flags (required, set, immutable) |
| Primary keys | pk |
Foreign keys + ON DELETE action | relates … on_delete cascade | restrict | set null |
| Unique & standard indexes | the indexes [...] block |
| Enum types | enum declarations |
| CHECK constraints | persistence-layer invariants (below) |
| Triggers | lifecycle/consistency rules that need them |
Persistence invariants become CHECK constraints
An invariant whose rule is over the row's own fields, targeting persistence, is enforced in the database as a CHECK constraint. Chowk's stock ledger can never over-reserve:
invariant StockCoversReservations "Reserved stock can never exceed available stock." {
expr "this.reserved <= this.available"
enforce_on [create, update]
target persistence
error "InsufficientStock"
}Chitragupt emits this as a CHECK (reserved <= available) on the stock_level table. The database itself refuses a violating row — a guarantee no application bug can bypass — and the framework maps the constraint violation back to the declared InsufficientStock error (a typed 409). You declared a business rule; it materialized as a database constraint.
Verifying an apply landed
After migrate, confirm the schema is really there rather than trusting the exit code — query the new tables (\dt <sub-domain>.*), check a constraint exists, insert a violating row and watch the CHECK reject it. A clean generate afterward (empty diff) is the proof the database now matches the desired schema exactly.
The migration workflow page covers the full CLI lifecycle — status, rollback, validation, and the deploy-time story — and Row-Level Security & roles covers how Chitragupt secures the tables it creates.