Building your app
Your app's database (migrations)
The migration files in your repo are the source of truth. There is no schema designer in the portal — there was one and it was removed, because it never wrote anything into your repo, so a drawn table became a third truth matching neither the repo nor the live database. You write an ordinary Laravel migration, push it, and import it.
The chain
database/migrations/ in your repo
│ "Import from repo" (or import_migrations_from_repo)
▼
the portal's migration store ──→ manifest.database.migrations[]
│
▼ provision_database (sandbox) / the portal button (production)
your app's own Postgres schema
Every link is required. Without the import, buildManifest() emits
requiresDatabase:false and migrations:[], and okta-web refuses to create
the database with "nothing to provision" — the import is also what flips
requires_database and fills database_schema on your versions.
File shape
database/migrations/2026_07_20_101500_create_exam_papers.php
└─────┬────────┘ └────────┬───────────┘
version migration name
YYYY_MM_DD_HHMMSS_<name>.php is mandatory. Anything that does not match
is skipped silently.
version is the key to everything after that: apply order, matching on
re-import, and the record of what ran. Re-timestamping an existing file
makes a NEW migration as far as the platform is concerned — it does not
edit the old one.
The baseline file
000000_create_<app>_database.phpThe boilerplate ships it starting with
000000deliberately: that name does not match the pattern above, so it is never imported. Creating the database itself is okta-web's step, and a file that does it again collides with it.But the skip is by name, not by intent. Rename it to a real timestamp (
2026_07_26_100000_create_<app>_database.php) and it becomes an ordinary migration: imported and shipped — usually with a timestamp older than your real migrations, which leads straight to the next error.
"Out-of-order migration" — and why it stops everything
Out-of-order migration "2026_07_26_100000" precedes already-applied
"2026_07_27_100001" on binding #21. Migrations are immutable once applied
— add a new corrective migration instead.
The applier walks migrations in version order and refuses to run one
older than the newest already applied to that database. The alternative
would be a lie: running an older migration after a newer one gives you a
schema no other install has ever passed through, and makes the apply order
differ between two databases claiming the same version.
The refusal aborts the whole run, not just that row. So one migration with a wrong timestamp freezes everything behind it: you read "7 applied, 1 pending" and nothing moves however often you press sync.
Getting out:
- If the row should never ship — the baseline file above being the textbook case — delete it from the list. It ran nowhere, so the deletion is completely clean and the block clears at once.
- If it is a real migration that arrived late with an old timestamp: give it a timestamp after the last applied one (a new file in your repo), then delete the old row and re-import.
- If it has already run somewhere, do not delete or edit it: add a new corrective migration. That is what "immutable once applied" in the error means.
Two statuses: draft and published
| Status | Where from | Editable | In the manifest |
|---|---|---|---|
published |
imported from the repo | ✗ SQL frozen | ✓ in every later manifest |
draft |
inline SQL typed in the portal | ✓ | ✗ until published |
The import writes published directly — these are real release files,
not drafts being iterated on. So "published" here does not mean somebody
decided to release it: a file that was incomplete the day you pushed it
arrives frozen.
Correcting a migration after it has arrived
Route one — a new migration. This is the normal one: an ALTER corrects
what its predecessor did. Published SQL is frozen for a reason — a school's
database has already run it, and rewriting it underneath makes what is in
that database differ from what the manifest claims.
Route two — delete. Available on every row, published or draft, from the database table in the portal. Know what it does and does not do:
- ✓ Removes it from every future manifest → new installs will not get it.
- ✗ Undoes nothing already applied. A table created last week stays created.
- ✗ Does not touch manifests already sent to okta-web; the difference starts at the next publish.
- ✗ Does not touch the applied ledger. That ledger says what a real database executed, and editing it to match a decision made afterwards would leave the platform unable to say what is in that schema.
So if the migration has run on existing databases, deleting creates a permanent divergence between them and any new install. The portal shows a "ran on N databases" badge on the row and changes the confirmation text accordingly — read it: it is the difference between tidying a list and two schemas that will never match again.
When is deleting the right answer? An incomplete or broken migration that has not run on any database, or a row whose file is no longer in your repo at all.
Rows whose file is gone
The import adds and updates; it never deletes. So if you delete a file from your repo, push, and re-import, you get "imported successfully" back while the deleted file is still shipping in every manifest.
That is why the import reports how many there are and badges those rows
"not in repo". They are not deleted automatically: the import runs
against one ref, a file absent from it may be absent only from that
branch, and it may already have run against a live database. The call is
yours, on the row.
Inline SQL in the portal
For small corrections that do not warrant a repo round-trip. Saved as
draft and published with the next version. Statements with no business in
an app migration are refused — DROP DATABASE/SCHEMA/ROLE/USER,
GRANT/REVOKE, ALTER ROLE/USER, COPY … FROM PROGRAM, and the
server-file functions. The real safety net is not that list but the isolated
schema: the connection role cannot touch another app's data at all.
MCP tools
| Tool | What it does |
|---|---|
list_migrations |
Migrations in apply order + the requires_database flag per version |
read_migration |
One migration's body — the copy the platform actually executes, which can drift from your repo |
import_migrations_from_repo |
The "Import from repo" button, and it reports missing rows |
provision_database |
Create the sandbox database and apply what has not been applied (production stays in the portal) |
database_status |
Whether the database is provisioned, the last run's outcome, applied vs pending |
diff_schema |
What your files describe against what is actually in the sandbox database |
diff_schema always states its own coverage: a migration stored as a repo
path carries no SQL here, so the checker cannot claim "undeclared" about
something it never read.