The migration workflow

Chitragupt is a CLI, and its subcommands are the lifecycle of a schema — from seeing what would change, to applying it, to auditing what's been applied. Its own summary:

Chitragupt compares your desired schema (from schema.json) against
the actual PostgreSQL database state and generates incremental migrations.

Commands:
  diff       Show schema differences
  generate   Generate migration files from diff
  migrate    Run pending UP migrations
  rollback   Rollback last N migrations
  status     Show applied/pending migrations
  create     Create empty migration template
  grants     Sync database roles and grants
  validate   Verify migration checksums

The core loop: diff → generate → migrate

  • diff connects to the live database and prints the differences between it and the desired schema, applying nothing. It is the read-only preview — "what would change" — and the way you confirm a database matches the model (a clean diff is zero differences).
  • generate turns that diff into a migration (UP to apply the delta, DOWN to reverse it). If there is no difference it writes nothing — "Schema is up to date."
  • migrate runs the pending UP migration against the database.

In local development these are chained for you: the docker stack's one-shot migration step runs generate then migrate, so a new slice's DDL appears the moment you bring the stack up — no separate step, nothing to remember.

Auditing and reversing

  • status shows which migrations have been applied and which are pending — the ledger of what the database has seen.
  • rollback reverses the last N migrations by running their DOWN SQL. Because generate emits both directions, a migration is reversible by construction.
  • validate verifies the checksums of applied migrations — it detects whether a migration's recorded content matches what actually ran, catching tampering or drift in the migration history.
  • create writes an empty migration template for the rare case you need a hand-authored step outside the derived diff.

Deploy-time: the same commands, run for you

The declarative model shines at deploy. There is no committed migration log to ship — the deploy runs generate against the production database (computing the delta for whatever it currently is) and then migrate, exactly as kalki_sys. An empty diff is a no-op, so redeploying an unchanged schema does nothing.

⚠️ The deploy job auto-applies whatever diff it computes — including destructive operations. A change to the desired schema that drops a column runs at deploy with no human in the loop. So a schema change is reviewed the same way code is: by reviewing the model (the entities) in the pull request, because that is what the migration is derived from. Preview the effect first with diff against a production-like database.

Roles and grants come with the migration

Schema is only half of a secure database; the other half is who can touch it. Chitragupt handles that as part of generate/migrate, not as a separate step: the diff includes a bootstrap CREATE ROLE for the least-privileged application role (when it doesn't yet exist), and every CREATE SCHEMA bundles the GRANTs that role needs on the tables Chitragupt creates. So the least-privilege boundary is maintained automatically as the schema grows — you don't run a command to sync grants; they ride the migration. (The grants subcommand is reserved for future standalone role syncing; today the role and GRANT emission lives in the diff.) Row-level security, the other half of the isolation story, is on the Security & roles page.

doctor — a pre-deploy health check

Because RLS silently no-ops if the request path runs as a superuser, Chitragupt ships a read-only doctor command that catches that trap before it bites. It probes both connection pools and reports findings by severity: it fails if the app pool's role is a superuser or has BYPASSRLS (every policy would be silently ignored), and warns on softer misconfigurations — the system pool not actually being elevated, a role identity that doesn't match what the datastore declares, single-role mode, or policies still targeting PUBLIC. Run it as a fast gate before trusting a deployment's isolation.

Safety: destructive operations are surfaced, not hidden

Chitragupt classifies every operation by risk — safe, warning (a column-type or nullability change), destructive (dropping a column, index, constraint, or policy), or irreversible (dropping a table, schema, or enum). At generate time destructive ops are printed with a ! marker and a warning banner, and the generated SQL carries inline -- WARNING [DESTRUCTIVE] comments. It also refuses to guess: adding a NOT NULL column with no safe default fails generation with a clear error rather than emitting SQL that would break on existing rows — you fix it in the model and re-generate. Preview anything with --dry-run, which shows what would run without executing it. (The diff + generate pair is the plan — there's no separate plan step.)

A pin bump needs the tool rebuilt

One operational note: Chitragupt is versioned with the framework. When you bump the framework pin, the chitragupt binary must be rebuilt at the new version — the deploy image builds it at $FRAMEWORK_VERSION, and running an older binary against a newer desired schema computes a stale diff. Match the tool to the pin.