Skip to content
prod e051e98
Browse

Database & migrations

The model behind a safe migration

On a CodeCanyon project, your database is shared territory: the vendor owns most of the tables and ships new migrations with every release, while you layer your own data on top. Get the migration model wrong and a routine vendor update collides with your work and breaks the app. Get it right and the update is a quiet, additive merge.

This guide teaches the mental model — how Laravel actually decides what to run, why “additive only” is the one rule that keeps you safe, and how a schema-diff tool catches a clash before it ships. The exact _zaj migration recipe and the step-by-step update procedure live elsewhere; this page is the why that makes them make sense.

Migrations feel like magic until you see the single comparison underneath them. When you run a migration, Laravel compares two lists and runs only the difference:

flowchart LR
A["Migration FILES on disk<br/>(what COULD run)"]
B["migrations table in the DB<br/>(what HAS run)"]
A --> Q{"In A but<br/>not in B?"}
B --> Q
Q -->|"Yes"| Run["Run it"]
Q -->|"No"| Skip["Skip it — never re-run"]
  • Source A — every migration file in your migration folders. Everything that could run.
  • Source B — the migrations table inside the database, which records everything that has run.

Anything already recorded in B — vendor’s or yours — is skipped, never re-run. Only files in A that aren’t yet in B actually execute. That one rule is the whole engine.

This is exactly why a vendor update is safe by default: your migrations have already run and are recorded, so they are skipped; only the vendor’s new files run. You can see the split yourself:

Terminal window
php artisan migrate:status
# Each row shows: migration name .... [batch] Ran / Pending
# Already-recorded rows = Ran (skipped). New files = Pending (will run).

The migrations table also carries a batch number — which “run” of the migrate command added each row. Batch 1 is usually the vendor’s first deploy; later batches are subsequent adds. Batches are what migrate:rollback uses to undo a group together.

Order matters — but only where there are dependencies

Section titled “Order matters — but only where there are dependencies”

A common worry is “did my migrations run in the right order?” The honest answer: order only matters when one table depends on another.

flowchart LR
users["users"] --> posts["posts<br/>(references users)"]
posts --> comments["comments<br/>(references posts)"]

A hard foreign key creates an ordering dependency: posts cannot be created before users exists, or the database rejects the constraint. Independent tables — ones that reference nothing — can run in any order. This is the deeper reason the handbook recommends soft references when you point at a vendor table: a soft reference (an indexed column, no constrained() foreign key) removes the ordering dependency entirely, so the vendor can reshape or recreate their table and your migration still runs.

SituationOrder-sensitive?Why
Two tables with no linkNoNothing references anything
Your table holds a hard FK to a vendor tableYesThe vendor table must exist first, in its expected shape
Your table holds a soft reference to a vendor rowNoJust an indexed column — no constraint to satisfy
Foreign keys between your own tablesYes, but you control itThe dependency is yours to order

The trade-off of a soft reference is that the database won’t enforce the link for you — your application code keeps it consistent. On a vendor-shared schema that is almost always the right trade.

Every safe database change on a CodeCanyon project obeys a single discipline: only ever add; never modify or drop what the vendor owns. Two mechanisms cover every legitimate need, and both live in your own migration files:

You need to…MechanismRisk
Store an extra attribute on a vendor rowA _zaj migration that adds a nullable column to the vendor tableManaged
Store a new feature’s own dataA zajm_ table your module ownsLow — the vendor doesn’t know it exists

The thing you never do: edit an already-run vendor migration, run a raw ALTER TABLE by hand, or modify/drop a vendor column. The full recipe — the _zaj filename suffix, the ZAJ: live-schema comment, the additive-and-reversible shape — is taught once in the schema-management handbook chapter, so it stays in one place.

The risk ladder, ranked safest to riskiest, makes the discipline concrete:

flowchart TD
L1["New zajm_ table you own — vendor never sees it 🟢"]
L2["zajm_ table with a SOFT reference to a vendor row 🟢"]
L3["Additive nullable _zaj column on a vendor table 🟠"]
L4["Modify or DROP a vendor column 🔴 never"]
L1 --> L2 --> L3 --> L4

The only real residual risk in that ladder is the orange rung: you add a column the vendor later adds too, in their own release — and a duplicate column makes the migration fail. That collision is precisely what a schema diff is for.

Verify with a schema diff — files lied, the database is truth

Section titled “Verify with a schema diff — files lied, the database is truth”

Because Laravel can’t see changes made outside its migration files, you need a second tool that inspects the actual database and compares two states. This is how you catch a vendor’s new column clashing with yours before you apply the update, and how you confirm staging matches production after a deploy.

The pattern is always the same three moves:

flowchart LR
snap["1 · Snapshot<br/>the current schema"] --> change["2 · Apply the change<br/>(vendor update / your migration)"]
change --> diff["3 · Diff the two<br/>— review before shipping"]

Snapshot before, snapshot after, diff the two. On a vendor update you snapshot before importing, run the vendor’s migrations locally, snapshot again, then diff — and look for: changes to a table you extended, a new vendor table whose name resembles one of yours, or a column-type change on a table you reference. New unrelated tables and new columns on tables you don’t touch are safe.

Three tools cover the common needs. They differ mainly in setup weight and whether a team needs a shared GUI:

ToolVisual diffSetupBest when
AtlasBrowser ERD + CLI (--web)One binary (brew install ariga/tap/atlas)Default for solo work — fast, git-trackable snapshots, lints dangerous changes
BytebaseFull web GUIDocker containerA team needs approval workflows, change history, and rollback in one place
DBDiffCLI onlyPHP, no dependenciesYou just want a quick MySQL-only compare with up/down SQL

“When to use which” in one line: reach for Atlas first — it’s the closest thing to a git diff for your database, runs from a single binary, and its snapshots commit cleanly into git. Step up to Bytebase only when a team needs governance (approvals, audit log, RBAC); drop to DBDiff only for a throwaway MySQL-to-MySQL comparison.

This guide is the model. When you’re ready to run the commands, the playbooks and handbook carry them: