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.php

The boilerplate ships it starting with 000000 deliberately: 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:

  1. 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.
  2. 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.
  3. 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.


View the whole guide on one page · ↧ Raw Markdown