YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Data Migrations
A code change that renames, moves or splits a field leaves the documents already stored in the old shape. A migration is a file in
script/migration/ that reshapes them, and the same file runs on your machine and in a deployed container.- It plans before it writes.
planchanges none of your data and reports what would change, andapplycan refuse when the data moved since that report. - Every run is recorded. The app's own database keeps which migration ran, when, with which params and, given
--actor, by whom, so each environment knows what it still owes. - It can be rolled back. Every write keeps the value it replaced. A rollback puts those back, deletes for good the rows the migration inserted or copied, and skips a row someone edited since.
Create And Run
From the workspace root, create a migration named for what it does:
Terminal
This writes
apps/koyo/script/migration/202610211000_orderToppings.ts. The file name is the migration's id, and its UTC timestamp keeps migrations in the order they were written. Run it with akan migrate:Terminal
| Action | What it does |
|---|---|
| list | The default. Each migration as pending, applied or partial, with the params it takes. |
| plan <id> | Runs plan and prints its summary, the first 100 items and a planHash. |
| apply <id> | Plans again, writes, then runs verify. |
| verify <id> | Runs verify alone. |
| rollback <id> | Undoes the apply from its change log. |
- Rerun one while you write it by rolling it back, editing, and applying it again.
Write One
A migration's default export is
migration(title) with three stages. This one moves each order's topping into a toppings list:apps/koyo/script/migration/202610211000_orderToppings.ts
planonly reports. It returns the report a person reviews:summaryto read, and optionallyitems, the decisionsapplyreceives as its second argument.verifyreturns problems, one string for each thing still wrong. It runs right after every apply, and an empty list means the data is as intended.- Skip rows already fixed. The
whereabove leaves out orders without atopping, so a second run changes nothing.
Params
A choice that belongs to whoever runs the migration, not to the code, is a param. It takes a scalar such as
String or ID, or an enumOf class, and its value is checked before plan runs:apps/koyo/script/migration/202610221400_orderStore.ts
Terminal
The third argument of
.param() takes:descString
What the value means, for whoever fills it in.
enumString[]
The values a
String may take.default
The value when the param is left out.
nullableBoolean
Left out, the value is
null rather than an error.Rows And Filters
table(refName) reads and writes a model's rows as stored, so a field the model no longer declares is still reachable. refName is the camelCase model name:| Call |
|---|
| ↳ What it does |
| find(where?, { after, limit }) |
Rows in id order, removed ones too (as in count and rewrite) unless { withRemoved: false }. |
| get(id) · count(where?) |
| One row, or how many match. |
| set(id, patch) · unset(id, paths) |
Writes or removes dot paths in one row's _doc. |
| rewrite(fn, { where }) |
Walks matching rows 500 at a time (batchSize); fn returns a patch, or nothing to skip the row. |
| insert(doc) · copy(id, patch) |
A new row; copy starts from another row's _doc. |
| remove(id) · restore(id) |
| A soft removal and its undo. |
A filter maps dot paths into
_doc, or a column such as removedAt, to a value or to $ne, $in, $nin, $exists, $gt, $gte, $lt or $lte. Every key must match.$neleaves out missing andnullvalues.{ status: { $ne: "done" } }skips rows whosestatusis absent ornull; find those with{ status: null }.updatedAtstays as it was. A migration reshapes data rather than editing it, so nothing sorted byupdatedAtmoves.- Beside
table, each stage getsparams,createId()for a new row's id,logger,sqlfor a raw read, andget(srv.IcecreamOrderService)for a service of the booted app, withsrvimported from../../server.
Review, Apply, Roll Back
On data others rely on, put a review between plan and apply.
plan prints a planHash with its report; hand it to apply with --expect, and the apply stops with planChanged if the plan it computes again no longer matches.Terminal
- The hash covers the params too, so apply with the params you planned with.
- A problem from
verifyfails the command with exit code 3 but leaves the migration applied. Roll it back and fix the file, or write a new migration once another environment has applied this one. - A rollback skips rows edited since. A row whose value changed after the migration wrote it is left as it is and listed in the result, so a later edit is never overwritten.
.rollback(fn)replaces the default when undoing needs a judgment, such as keeping a copied row others now point at. It walkschangeswithfor await, newest first, and callschanges.revert(change)orchanges.skip(change, reason).- One run at a time. While one runs, a second from another terminal or replica is refused.
Large Tables
An apply runs in one transaction, so a failure leaves nothing half done. On SQLite that transaction holds the only write lock, and once it passes about five seconds the live server's writes start to fail. For a table that takes longer, commit as you go:
apps/koyo/script/migration/202610211000_orderToppings.ts
- Each
rewritebatch commits on its own, with the point it reached. A failed apply leaves the migrationpartial: apply again with the same params to resume there, planning it again first if you pass--expect, or roll back what it wrote. - Write it so a second run changes nothing. A resumed apply runs
applyfrom the top again. - The change log takes disk. Every changed row keeps its old and new values, about 160 bytes plus the values themselves.
In A Container
The built image carries every migration, so nothing is copied into a container. There,
bun main.js ops migrate takes the same actions and options as akan migrate, without the app name:Docker
Terminal
Kubernetes
Terminal
--jsonprints one JSON line last, withokand, on failure, acodenaming why. The exit code is 0 on success, 2 for a mistake in the command, 3 when verify finds problems and 1 otherwise,alreadyAppliedincluded.--actornames who ran it in the run's record.- It boots a second server beside the one serving traffic, with no port and no schedules, and stops when the action ends.
Writing Rules
A migration changes production data that a rollback may have to put back. Four habits keep that possible:
- New code meets old data first. A deploy serves traffic before anyone applies the migration, so the new code must read both shapes until it is applied.
- Write through
table(), not services orsql. A service write runs the model's hooks, which may notify users, and neither reaches the change log, so a rollback cannot undo it. - Call nothing outside the database. A rollback can put rows back, but it cannot unsend an email or refund a payment.
- Throw a plain
Errorwith a message an operator can act on. The run's record keeps it.


Delete a migration once every environment has applied it. Each database keeps its record, and the change log can still roll it back without the file, but only with the default undo, so keep a file whose
.rollback(fn) you may still need.