Forward-only SQL migrations for iii. You write plain .sql
files, miiigrate applies and tracks them through the
database worker — it never opens
a database connection of its own. It can also generate TypeScript types that
describe your schema exactly as it arrives over the wire.
you / an agent / CI
│
│ iii trigger migrate::up | status | create | codegen
▼
┌──────────────┐ database::query / execute / transaction
│ miiigrate │ ──────────────────────────────────────────▶ ┌──────────┐
└──────────────┘ (no direct DB connection) │ database │──▶ Postgres
│ │ │ worker │──▶ SQLite
│ reads │ writes └──────────┘
▼ ▼
migrations/*.sql db.types.ts
The whole workflow is four functions, used in this order:
migrate::create ──▶ write your SQL ──▶ migrate::up ──▶ migrate::codegen
(scaffold) (plain .sql) (apply) (TypeScript)
iii worker add database
iii worker add miiigratemiiigrate requires the database worker: every read goes through
database::query, every migration through one atomic
database::transaction. Point the database worker at your database
(SQLite or Postgres), start the engine with iii, and the four
migrate::* functions are available.
Using an AI agent (Claude Code, Cursor, …)? Install the agent skill so it knows when and how to drive miiigrate:
npx skills add aircodev/miiigrate# 1. scaffold a migration file
iii trigger migrate::create --json '{"name":"create_users"}'
# -> ./migrations/20260719143000_create_users.sql
# 2. write your SQL in that file, then apply everything pending
iii trigger migrate::up --json '{}'
# -> { "applied": ["20260719143000_create_users.sql"], "skipped": 0, "duration_ms": 6 }
# 3. check the state at any time (read-only)
iii trigger migrate::status --json '{}'
# -> { "applied": [...], "pending": [...], "mismatched": [], "missing": [] }
# 4. regenerate TypeScript types after every schema change
iii trigger migrate::codegen --json '{}'
# -> ./db.types.ts — one interface per tableThree rules keep it simple and safe:
- Forward-only. Never edit an applied file — write a new migration to change the schema again. Edits are caught by checksum before anything runs.
- Atomic. Each file runs as one transaction: it fully applies or fully rolls back.
- Concurrency-safe. With
auto: true, every replica can runmigrate::upat startup; they serialize on a lock and losers report skipped, not failed.
workers:
- name: database
config:
databases:
primary:
url: sqlite:./data/app.db # or postgres://…
- name: miiigrate
config:
db: primary # database name in the database worker
dir: ./migrations # folder of YYYYMMDDHHMMSS_slug.sql files
auto: false # true = run migrate::up at startup
types_out: ./db.types.ts # codegen output (optional)The config: block is a first-boot seed; afterwards settings live in the
configuration worker under id miiigrate. Details and defaults:
docs/configuration.md.
| Page | What it covers |
|---|---|
| Functions | migrate::up, status, create, codegen — payloads and responses |
| Configuration | Every key, defaults, seed vs runtime config |
| Workflows | Local dev, production auto: true, table and column recipes |
| Codegen | TypeScript type mapping, wire-format rules |
| Errors | Every error code and how to recover |
| Changelog | Releases |
| Roadmap | What is planned, and what is not |
The playground/
directory boots a real engine plus database worker and runs the full scenario:
run.sh
(SQLite end-to-end) and
run-postgres.sh
(Postgres 16, two concurrent instances proving lock serialization).
Apache-2.0