Build Extensions
Add Isolated Migrations
Use extension migrations when an extension needs its own durable storage. The migration runner keeps extension storage isolated by table prefix and tracks applied versions in a vendor-owned migration table.
Goal
Create a migration that adds:
my_vendor_notes
and is tracked in:
my_vendor_schema_migrations
Prerequisites
- The extension vendor is snake_case, such as
my_vendor. - The extension has a valid
manifest.json. - The operator can register, enable, and grant the extension.
- The migration only creates or changes extension-owned tables.
Human Path
Set hasMigrations to true:
{
"vendor": "my_vendor",
"name": "My Vendor",
"version": "0.1.0",
"requiredScopes": ["content.read"],
"hasMigrations": true,
"adminRoutes": []
}
Create:
app/extensions/my_vendor/migrations/v001_create_notes_table.php
Return a versioned SQL list:
<?php
declare(strict_types=1);
return [
'v001_create_notes_table' => [
'CREATE TABLE IF NOT EXISTS my_vendor_notes (
id VARCHAR(32) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
created_at VARCHAR(40) NOT NULL
)',
],
];
When the extension loads, ExtensionLoader calls ExtensionMigrationRunner before requiring bootstrap.php.
AI Path
An AI coding agent can create or edit:
app/extensions/my_vendor/manifest.jsonapp/extensions/my_vendor/migrations/v001_create_notes_table.php
The agent can run the foundation verification command and inspect the migration file for prefix compliance.
An installed MCP client cannot create migration files, call the migration runner directly, or bypass extension registration. After the operator loads the extension, MCP or API clients may call exposed actions that use the extension-owned tables, if those actions exist and scopes allow them.
Migration Loading
The loader reads:
app/extensions/{vendor}/migrations/*.php
Files are sorted by filename. A migration file may return:
- an array of
version => list<string>SQL statements, - an array of
version => callable, - one callable, where the version is derived from the filename.
Array SQL migrations are checked for table isolation before each statement runs.
Scopes
Migration execution is not a caller-scoped API operation. It happens during extension load.
The extension still must pass the manifest grant gate. If the manifest declares content.read, the operator grant must include content.read; otherwise the loader rejects or skips the extension before runtime contributions are available.
Artifacts
Local files:
app/extensions/my_vendor/manifest.jsonapp/extensions/my_vendor/migrations/v001_create_notes_table.php
Database tables:
my_vendor_notesmy_vendor_schema_migrations
Runtime source:
MyronExtensionLoader::boot()MyronExtensionMigrationRunner::run()
Safety Boundary
Keep every extension table under the vendor prefix:
{vendor}_*
Do not create, alter, drop, or index core tables. Do not write migrations that depend on real secrets or local-only absolute paths. Do not run migration files outside the extension loader as a workaround for grant or load failures.
Verification
Run:
php app/tests/verify-extension-system-foundation.php
The foundation test verifies the reference extension behavior:
myron_sample_pingsis created,myron_sample_schema_migrationsis created,- a migration that tries to create
core_table_violationis rejected.
For your extension, verify that:
- the manifest has
hasMigrations: true, - the migration file returns versioned migrations,
- all table names start with
{vendor}_, - the extension is registered and enabled,
- the vendor tracking table contains the applied version.
Failure Modes
| Failure | Likely surface | Safe fix |
|---|---|---|
| Migration does not run | hasMigrations is false, extension skipped, or file is missing. | Enable hasMigrations, register the extension, and confirm the file path. |
| Version runs repeatedly | Version key changes between boots. | Keep stable version keys such as v001_create_notes_table. |
| Non-prefixed table rejected | Migration runner isolation check. | Rename the table under {vendor}_*. |
| Bootstrap never loads | Migration threw before bootstrap. | Fix migration failure; do not skip tracking manually. |
| MCP cannot run migration | Expected behavior. | Migrations are local/runtime loader behavior, not MCP tools. |