Backend database migrations (0.5)
SkillDatabases & dataUse 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/, 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/.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Backend database migrations (0.5) skill
What this skill tells your AI
The instructions your AI receives, as published by tale-project/tale in .agents/skills/create-migration/SKILL.md and read by ahel’s review.
Every database-shape change ships as a numbered SQL file under
services/platform/backend/db/migrations/. 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.sql
NNNNis 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; it must be idempotent and bounded. |
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.
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.bunx vitest --run --project server— the unit layer for the service that reads the new shape.
Definition of done
- One numbered
.sqlfile, no gap, never renamed after shipping - 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
Signals
- GitHub stars
- 29
- Forks
- 5
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
create-migration-tale-project- Source
- github.com/tale-project/tale