Migrations
How Dyrected keeps your database in step with your schema — automatic schema sync, safe field renames, and pushing schema to Dyrected Cloud.
Schemas change. You add a collection, rename a field, give something a default. In a lot of tools that means writing migration files by hand. Dyrected takes a different approach: it keeps the database schema in step with your config automatically, and gives you a few field-level tools for the changes that need care. There is no migration-file workflow and no migrate command to learn.
It helps to separate two things that both get called "migrations":
- Schema changes — the tables and columns your database needs. Dyrected handles these for you.
- Content-contract changes — renaming a field, adding a default, reshaping data that already exists. These are yours to manage, with the tools below.
Automatic schema sync
On startup, the SQL adapters (Postgres, SQLite, MySQL) bring the database in line with your config. They create a table for any collection that doesn't have one yet, and add a column for any newly promoted field. Add a collection or a promoted field, restart, and the schema is ready.
This sync is deliberately additive. It creates missing tables and columns; it does not drop columns, rename them, or change their types. That keeps your data safe by default, and it's why the destructive changes are the ones you have to think about. MongoDB is schema-flexible, so it needs no sync step at all.
Renaming a field safely
Renaming a field is the classic risky change: the new name has no data, and the old data is stranded under the old key. Dyrected handles this with renameTo plus a default. Rather than rewriting every document at once, it reads the old value under the new name whenever a document is loaded.
Set the field's name to the new key, and point renameTo at the previous key it should fall back to:
import { defineCollection, defineTextField } from '@dyrected/core'
export const Customers = defineCollection({
slug: 'customers',
fields: [
defineTextField({
name: 'fullName',
label: 'Full name',
renameTo: 'name',
defaultValue: '',
required: true,
}),
],
})Here the field is now called fullName, and renameTo: 'name' tells Dyrected to read the old name value when a document has no fullName yet. This happens at read time: load a document and the old value surfaces under the new name, so your app and the admin panel see the right value straight away. The value is physically rewritten under the new key the next time that document is saved with the new field present — for example, when someone edits and saves it in the admin panel. Documents nobody touches keep their value under the old key, which is exactly why you leave renameTo in place.
Keep renameTo until you've confirmed every production document has been written under the new key. Remove it too early and documents that still only have the old key will look empty.
Defaults and backfilling
defaultValue ensures documents always have a value when a field is omitted. When creating new documents via the SDK, API, or admin forms, Dyrected applies the defaultValue automatically before writing to the database:
defineSelectField({
name: 'status',
label: 'Status',
options: ['pending', 'active', 'archived'],
defaultValue: 'pending',
})Some field types also fall back to sensible empty defaults when omitted — booleans default to false and array fields default to an empty list.
Backfilling defaults on existing collections
When you add a new defaultValue to an existing collection that already contains historical records, a subtle schema/data drift occurs:
- In-memory reads surface the default value when loading documents.
- Database grouping and filtering (such as operational view Kanban columns, grouped tables, or raw SQL queries) query the physical database directly, where historical rows still store
NULL. Consequently, historical records may appear in an "Unassigned" bucket rather than their expected default status column.
To align historical records with your new default, run a one-time backfill migration:
// scripts/backfill-status-defaults.ts
import { client } from "../server/db";
async function main() {
const { docs } = await client.collection("orders").find({
where: { status: { equals: null } },
limit: 1000,
});
for (const doc of docs) {
await client.collection("orders").update(doc.id, {
status: "pending",
});
}
console.log(`Backfilled status default on ${docs.length} orders.`);
}
main();Or execute a direct SQL query against your database adapter:
-- PostgreSQL
UPDATE collection_orders
SET data = jsonb_set(data, '{status}', '"pending"')
WHERE data->>'status' IS NULL;Promoting a field on an existing collection
Adding promoted: true (or adding a field to an indexes array) creates a dedicated physical column during the next startup schema sync.
On SQL adapters (Postgres and MySQL), Dyrected automatically backfills existing records by extracting their values from the JSON document body into the newly promoted column. That means filtering and sorting against the freshly promoted field immediately indexes both historical and newly written rows without requiring a manual migration script. If a column was originally created as TEXT before an index was applied, Dyrected automatically widens it to VARCHAR(191) before establishing the index.
Pushing your schema to Dyrected Cloud
Self-hosted apps sync their schema at startup. Cloud projects push it explicitly with the CLI:
npx dyrected sync:schemaThis sends your collections, globals, and admin config to your Cloud site. It reads your API key and site ID from DYRECTED_API_KEY and DYRECTED_SITE_ID (or --api-key and --site-id flags), and regenerates your TypeScript types afterward unless you pass --skip-types. In CI you can pass --skip-on-error so a failed sync doesn't fail the build.
Before a production schema change
Whichever adapter you use, treat a schema change in production with care:
- Back up the database using your provider's recovery procedure.
- Review new promoted-field types and any uniqueness expectations against the values already stored.
- Run the change in a staging environment with production-like data first.
- Deploy code that can read both the old and new shape during a rolling release, so requests served mid-deploy don't break.
For the deployment side of this, see Deployment.