create-migration
Skill42 starsChanged 12 days agoMITView on GitHub
Use this skill whenever you change the shape of the 0.5 backend database — a new table, a new column, an index, a constraint, a backfill, or a data reshape. It owns the authoring contract (one numbered .sql file under services/platform/backend/db/migrations/ — or a numbered .ts data migration when a backfill must decide with the app's own code — applied at boot in filename order inside one advisory lock), the forward-only doctrine (every migration must be safe to apply to a live deployment mid-roll, because the previous image is still serving while the new one migrates), the org-config file lane (config trees are NOT database rows — they move through the scaffolder), and the proof duty (the real-Postgres integration check). Load it before adding anything under backend/db/migrations/.
11 other projects use a version of this.
Adopted by
Most recently taken up by
- anotherben/claude-harnesstheir own version, first seen Nov 8, 2026
- GoogleCloudPlatform/database-migration-agentstheir own version, first seen Nov 1, 2026
- GoogleCloudPlatform/database-migrationtheir own version, first seen Nov 1, 2026
- drferdi/Monorepo-safrstheir own version, first seen Oct 20, 2026
- buildpad-ai/skillstheir own version, first seen Oct 16, 2026
- owid/owid-graphertheir own version, first seen Oct 8, 2026
Other versions11 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 11
- OpenCTI-Platform/opencti★ 10ksame name
- fullstackhero/dotnet-starter-kit★ 6.9ksame name
- Notifuse/notifuse★ 2.3ksame name
- c9s/bbgo★ 1.7ksame name
- owid/owid-grapher★ 1.6ksame name
- drferdi/Monorepo-safrs★ 9same name
- anotherben/claude-harness★ 7same name
- GoogleCloudPlatform/database-migration-agents★ 3same name
- GoogleCloudPlatform/database-migration★ 2same name
- buildpad-ai/skills★ 1same name
1 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository9 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- write-docsSkill
- write-translationsSkill
- create-migrationSkill
- write-docsSkill
- write-translationsSkill
The file
We split this skill into the rules it gives an agent
3 rules. 0 are common, 1 is distinctive, and 2 are very specific to this project.
0
Common
Most of the 11 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 8
Every database-shape change ships as a numbered SQL file under services/platform/backend/db/migrations/ (a backfill that must decide with …
- Very specific: line 64
Use IF NOT EXISTS / IF EXISTS freely — a migration file runs once, but a re-run after a half-failed deploy must not be a landmine.
- Distinctive: line 142
Backend integration: check with it
“Common” is measured: kept by at least 6 in 10 of the 11 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Backend database migrations (0.5)
Every database-shape change ships as a numbered SQL file under
services/platform/backend/db/migrations/ (a backfill that must decide with the app's own code
is a numbered .ts data migration there — see below). runBootMigrations
(backend/db/migrate.ts) applies them at every backend boot, in filename order, each in its
own transaction, tracked by filename in app_migrations — all inside one session-scoped advisory
lock, so N concurrently booting containers (api + worker, or scaled replicas) apply everything
exactly once while the others wait.
There is no tale migrate up/down, no versioned framework, no rollback ledger: a deployed image
is at its own schema by construction. tale migrate means something else entirely — re-provision
built-in defaults into every org (/api/control/provision).
The 0.4 Convex versioned-migration framework (
defineDbMigration,migrations:runAll,tale migrate status/up/down, the world corpus) is retired. 0.5 is a fresh instance and carries no data forward from it.
The authoring contract
services/platform/backend/db/migrations/NNNN_snake_case_subject.sqlNNNNis the next zero-padded number, no gaps, no reuse. Filename order IS apply order, and the filename is the identity recorded inapp_migrations— never rename a file that has shipped, or every existing deployment re-applies it.- One subject per file. The name says what it is (
0057_competence_records.sql), not what you did (0057_fix.sql). - Everything lands in the
appschema (CREATE TABLE app.x), the app's own namespace. Better Auth owns the unqualified tables ("user","member","organization") and migrates itself; pg-boss ownspgboss. Never write either from here. - Comment the WHY at the top, and on any column whose meaning is not obvious from its name —
these files are the schema's documentation. Look at
0057_competence_records.sqlfor the house style (what the table is for, which rule an index encodes, why a row is retained rather than deleted). - Timestamps are
bigintepoch-millis columns named*_at_ms(the app's clock is JS).id text PRIMARY KEY DEFAULT gen_random_uuid()is the standard key.
Forward-only, and safe to apply under a rolling deploy
The previous image keeps serving while the new one migrates, so every migration must leave the OLD code working. That is the whole discipline:
| Change | How |
| New table | Just create it. |
| New column | Nullable, or NOT NULL DEFAULT …. Never bare NOT NULL on a populated table. |
| Retire a column | Stop reading it in code and ship that FIRST; drop it in a later release. |
| Rename a column | Two steps: add the new one + backfill, ship the code that writes both, then drop the old. |
| New constraint | Only if existing rows already satisfy it — otherwise clean the data in the same file, first. |
| New index | Plain CREATE INDEX (each migration is one transaction, so CONCURRENTLY is unavailable). |
| Backfill | Set-based UPDATE … WHERE in the same file; idempotent and bounded (.ts: see below). |
Encode the rule in the schema when you can. A partial unique index that says "at most one live grant per member" is a rule the database cannot forget; the same rule written as a scan-and-compare in a service is a rule the next handler will miss.
Use IF NOT EXISTS / IF EXISTS freely — a migration file runs once, but a re-run after a
half-failed deploy must not be a landmine.
Data migrations in TypeScript
A backfill whose decision is a rule the application already owns — which files an extractor reads
(isSupported()), which MIME types are media — must not freeze a SQL copy of that rule: the copy
is a second source of truth the moment the real one moves. Write it as a numbered .ts module in
the same directory instead:
services/platform/backend/db/migrations/NNNN_snake_case_subject.ts- It exports
migrate(tx: TransactionSql): Promise<void>. The migrator imports it by filename and runs it inside the migration's own transaction, in the same filename order and the sameapp_migrationsledger as the.sqlfiles (isMigrationFileinbackend/db/migrate.ts; a.test.tsor.d.tsbeside it is never applied). - Same rules as SQL: WHY at the top, idempotent, bounded, rolling-deploy safe. Read only the rows
that can need the fill, lock them (
FOR UPDATE), re-check the condition in theUPDATE, and write set-based (unnestof the ids and values the app's rule decided). A status write that a list renders emits its hint once per organization, and only for rows a list shows — written in the migration as anINSERT INTO app_realtime.outbox, like every other statement (below). - Import only pure rules; write every statement in the file. A data migration runs against the
schema as it stood at its own number, but with the code of whichever image applies it — a
database that jumps past several releases runs it with the newest. Anything that runs SQL
follows TODAY's schema: a domain service,
emitHintInTxfromrealtime/outbox.ts, a job enqueue, a SQL fragment such asHELD_BY_DOCUMENT_SQL. Import one, and the release that reshapes its table and updates it runs the new SQL on the old table — the migration fails, or quietly does something else. Import only what decides from its arguments and runs no SQL and no I/O —isSupported()/isImageFile(), theRAG_ERROR_*codes, the sentence helpers inbackend/core/knowledge/rag_unsupported.ts— and write the reads, the writes, anEXISTSprobe and the hint'sINSERTyourself.backend/db/data-migrations.test.tswalks every data migration's imports, transitively, and fails on a module outside itsPURE_RULESlist and on a package outside itsPURE_PACKAGESlist (node:fs,@tale/shared/db/…, or a runtimepostgres— onlyimport typefrom it is admitted); a rule module or a package joins its list only once it is known to be pure, with why. - Schema changes stay
.sql— those files are the schema's documentation. Scaffold withbun run gen:migration, kindts;0128_rag_unsupported_type_codes.tsand0129_rag_unsupported_image_codes.tsare the reference — each fills the code the indexer's rule implies on rows written before every lane wrote it. A fill that grows once its migration may have run anywhere (amainimage boots it too, not only a release) takes a new number, as0129did, never an edit of the applied file: a database that has it never runs it again. - Prove it the way SQL backfills are proven: seed the rows in
backend/integration-check.ts, import the module and runmigratein a transaction twice (the second run changes nothing), and pin its decision table with a unit test inbackend/db/data-migrations.test.ts, which also guards that every.tsmigration exportsmigrateand reaches only pure rules.
What does NOT belong here
- Org config files (agents, automations, connectors, providers, skills, governance policies)
live on the config volume, not in Postgres. They move through the org scaffolder
(
backend/domains/organizations/scaffold.ts), which is idempotent per domain and re-runnable viatale migrate/tale deploy --override-all. - The knowledge corpus schema has its own migrations under
services/db/migrations/knowledge-db/, applied byensureDefaultCorpusSchema(); a BYO corpus bootstraps on first use. - pg-boss queues — declared in
backend/jobs/boss.ts, created byensureQueues.
Prove it
A migration is not done until something exercises the shape it created:
bun run --filter @tale/platform backend:integration— the real-Postgres proof. It runs boot migrations twice CONCURRENTLY (the advisory lock's own test) and then drives every domain over the real schema. Add a probe for the behaviour your migration enables; see the backend README for the throwaway-Postgres + MinIO invocation. CI's Backend integration check runs it on the pull request against thetale-dbimage built from it, and fails on any lane that did not run.bunx vitest --run --project server— the unit layer for the service that reads the new shape.
Definition of done
- One numbered
.sqlfile (or.tsdata migration), no gap, never renamed after shipping - A
.tsdata migration writes every statement itself and imports only pure rules - Applies cleanly to a FRESH database and to one at the previous release
- The old code still works against the new schema (rolling-deploy safe)
- Rules that can be constraints/indexes are constraints/indexes
- A probe in
backend/integration-check.tscovers what it enables bun run --filter @tale/platform backend:integrationgreen, and the pull request's Backend integration check with it
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

