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.
How Laravel decides what runs
Section titled “How Laravel decides what runs”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
migrationstable 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:
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.
| Situation | Order-sensitive? | Why |
|---|---|---|
| Two tables with no link | No | Nothing references anything |
| Your table holds a hard FK to a vendor table | Yes | The vendor table must exist first, in its expected shape |
| Your table holds a soft reference to a vendor row | No | Just an indexed column — no constraint to satisfy |
| Foreign keys between your own tables | Yes, but you control it | The 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.
Additive only — the one rule
Section titled “Additive only — the one rule”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… | Mechanism | Risk |
|---|---|---|
| Store an extra attribute on a vendor row | A _zaj migration that adds a nullable column to the vendor table | Managed |
| Store a new feature’s own data | A zajm_ table your module owns | Low — 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 --> L4The 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.
Which schema-diff tool
Section titled “Which schema-diff tool”Three tools cover the common needs. They differ mainly in setup weight and whether a team needs a shared GUI:
| Tool | Visual diff | Setup | Best when |
|---|---|---|---|
| Atlas | Browser ERD + CLI (--web) | One binary (brew install ariga/tap/atlas) | Default for solo work — fast, git-trackable snapshots, lints dangerous changes |
| Bytebase | Full web GUI | Docker container | A team needs approval workflows, change history, and rollback in one place |
| DBDiff | CLI only | PHP, no dependencies | You 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.
Where to execute this
Section titled “Where to execute this”This guide is the model. When you’re ready to run the commands, the playbooks and handbook carry them: