Skip to content
prod e051e98
Browse

Schema management

Extending the database safely

The same seam that keeps your code update-safe must also exist in your database. The vendor owns their tables and ships new migrations with every release. The golden rule: never edit a vendor migration. Make every database change in your own additive migration instead — one the vendor’s update will never touch.

This chapter explains how Laravel migrations actually run, why that makes vendor updates safe, and the two ways you add data: a _zaj migration that adds marked columns to a vendor table, and a zajm_ table that your module owns.

Migrations feel like magic until you see the one comparison underneath them. When you run a migration, Laravel compares two lists:

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

Laravel runs only the files in A that aren’t yet recorded in B. Anything already run — vendor or yours — is skipped, never re-run.

This is exactly why vendor updates are safe: when the author ships a new version, your migrations have already run and are recorded, so they’re skipped. Only the vendor’s new files run — and because you never edited a vendor migration, there’s nothing of yours for their update to clobber.

There are only two safe ways to add data, and both live in your own migration files — never in the vendor’s:

You need to…MechanismWhere
Store extra columns on a vendor table (e.g. whitelabel_domain on users)_zaj migration — an additive migration that adds the columns to the vendor tabledatabase/migrations/{date}_add_{cols}_to_{table}_zaj.php
Store a new feature’s own datazajm_ table — a new table your ZajModule ownspackages/ZajModules/<Module>/Database/Migrations/zajm_{module}_{noun}

The one thing you never do: edit a vendor migration, or run raw ALTER TABLE SQL by hand.

Adding columns to a vendor table — the _zaj migration

Section titled “Adding columns to a vendor table — the _zaj migration”

You need a whitelabel_domain field on the vendor’s users table. You do not edit the vendor’s create_users_table migration. You write a separate additive migration with the _zaj suffix that adds the column to users:

database/migrations/2026_04_13_add_whitelabel_domain_to_users_zaj.php
// ZAJ:FILE — vendor table extension (_zaj). Additive only; reversible.
return new class extends Migration {
public function up(): void {
Schema::table('users', function (Blueprint $table) {
$table->string('whitelabel_domain')->nullable()->after('email')
->comment('ZAJ: ResiDoro v10.4 2026-04-14 — whitelabel domain');
});
}
public function down(): void {
Schema::table('users', fn (Blueprint $table) => $table->dropColumn('whitelabel_domain'));
}
};

Three markers make the column yours and auditable at every layer:

  • Filename — the _zaj.php suffix. find database/migrations -name '*_zaj.php' lists every extension you’ve made.
  • File header — a ZAJ:FILE comment, so a content scan finds it.
  • Live-schema comment->comment('ZAJ: …') lives in the running database itself, so it survives even if the migration file is deleted or a rollback wipes the filesystem markers.

When the data is genuinely new (not an attribute of a vendor row) — a loyalty ledger, GA4 events — it belongs to a ZajModule, in its own table named zajm_{module}_{noun}. These are brand-new tables the vendor doesn’t know exist, so they carry the lowest risk of all.

zajm_loyalty_points ← your module's table (vendor never touches it)
user_id → reference to users.id
points

A module table you fully own can use a normal constrained() foreign key between your own tables. The care comes only when you point out at a volatile vendor row — see soft references below. New zajm_ tables also get a table-level COMMENT 'ZAJ: …'; vendor tables you merely extended with a _zaj column do not (that would misleadingly claim the whole table as yours).

Ranked safest → riskiest, with the sanctioned move for each:

ChangeRiskSanctioned move
New zajm_ table of your ownLowPreferred — the vendor doesn’t know it exists
A zajm_ table that references a vendor rowMediumUse a soft reference, not a hard FK
Add a nullable column to a vendor tableManagedA marked _zaj migration (additive) + Atlas diff on update
Modify or drop a vendor columnCriticalNever — find an additive version of the change
Section titled “Soft references — don’t hard-link to a vendor table”

When one of your tables points at a vendor row, resist the database-level foreign key. A hard FK creates an ordering dependency: your table now requires the vendor table to exist and keep its shape. If the vendor drops or recreates that table during an update, your migration breaks.

A soft reference — a plain user_id column with an index, but no constrained() foreign key — stores the same link without the dependency. The vendor can reshape their table freely and your migration still runs.

Hard FK → constrained() → breaks if the vendor reshapes the referenced table
Soft ref → indexed column → survives whatever the vendor does

Trade-off: a soft reference means the database won’t enforce the link for you, so your application code keeps it consistent. (Foreign keys between your own zajm_ tables are fine — the dependency is yours to control.)

One subtle trap: Laravel’s migration system only knows about migration files. It cannot see a manual SQL change or a raw db.sql import the vendor shipped — it assumes the files tell the whole story.

So use the two tools for what each is good at: use Laravel migrations to make changes, and use a schema inspector (like Atlas) to verify the actual database state and diff your schema against a new vendor release. One makes the change; the other confirms reality matches and flags clashes.

You want to…Do this
Add a brand-new feature’s dataA zajm_{module}_{noun} table (lowest risk)
Store an extra attribute on a vendor rowA marked additive _zaj migration adding the column to the vendor table
Point one of your tables at a vendor rowSoft reference (indexed column), not a hard FK
Modify or drop a vendor columnStop — find an additive version of the change