Migration locations
Package migrations are auto-loaded by their ServiceProviders.
Creating a migration
For a package
For the main application
Migration conventions
Table naming
- Use plural snake_case:
products,order_items,variant_prices - Pivot tables use alphabetical singular:
product_category
Column conventions
Always include
- KSUID primary key —
$table->string('id')->primary() - Tenant scoping —
company_idand/ororganization_idwith indexes - Timestamps —
$table->timestamps() - Indexes — On foreign keys and commonly filtered columns
Foreign keys
Running migrations
PostgreSQL-specific features
Stored procedures
Custom PostgreSQL procedures live indatabase/procedures/:
cashing_range_list.pgsql— Generates cashing reports by date range
JSON columns
PostgreSQL’s JSONB type is used for flexible data:Seeding
Running seeders
Available seeders
Company to IAM scope upgrade
This branch adds clients without moving company business data to a new owner. The permission team key changes only iniam_roles, iam_model_has_roles and
iam_model_has_permissions, from company_id to scope_id.
iam_role_has_permissions is unchanged. Existing scope IDs equal company IDs.
Ten pending migrations extend the historical company schema. They create clients
and memberships, add XOR owners to profiles and assignments, create IAM scopes,
add client sessions, backfill membership organizations and constrain invitations,
agents and refresh rows. The migrations expect the schema produced by the preceding
migrations. They do not guess whether a column was manually renamed.
Before migrating
Back up PostgreSQL and verify restoration in an isolated database. Check migration history against the deployed company version. A database created from an intermediate client version of this development branch is not the historical company upgrade tested here; reconcile its history explicitly. Audit existing rows with read-only SQL before applying stricter constraints. All counts below must be zero. Investigate any result; do not silently discard or rewrite historical data to make a migration pass.company_id remain valid and keep a null scope_id.
Deploy in maintenance
Stop HTTP writes and other job producers. Drain pending jobs with the old code, especiallyclient_refresh, before stopping workers and Reverb.
The serialized refresh job now uses scopeId and scopeType instead of companyId;
worker restart alone is not a conversion of queued jobs.
Deploy code and configuration together, then run in the application container:
APP_KEY and JWT_SECRET. Do not generate new keys during
an upgrade. Do not use migrate:fresh or the demonstration seeder on existing data.
Both client guard discoveries must finish before client onboarding opens.
They do not require rediscovering company packages or modifying custom company roles.
Start workers and Reverb with the new code. Do not run old code against the renamed
permission schema. Verify login, switch, effective company permissions, old invitation
acceptance, client signup and guard boundaries before reopening writes.
After migration, check that each company has its same-ID scope, memberships have
the owner’s organization, old roles and pivots retain their IDs and grants, and
historical rows have no client owner. Password and company session hashes must
remain unchanged; new client session hashes start null.
Rollback boundary
Rollback and reapplication were tested before writing any client data. After client writes, old non-null company columns cannot represent those rows. Use a tested backup-restoration plan and account for subsequent writes. Do not delete client records just to force a rollback.Delivery checks
On 2 October 2026, PHP 8.4 and PostgreSQL 16 isolated containers upgraded a populated database created with company-only commit4efc9f9f. It contained two organizations,
four companies including children, human memberships, profiles, roles, temporary
permissions, agents, invitations, refresh rows and accounting/catalog/customer data.
Exact comparisons preserved 9,320 historical rows in 25 populated tables after
upgrade, rollback and reapplication. A pg_dump restoration into a separate database
also preserved that snapshot. Existing human and agent JWTs kept their effective
rights; a historical JWT without a session key received 401.
Old invitation acceptance and first-client onboarding worked after upgrade.
The full suite passed 1,439 tests and 9,719 assertions, with five PHPUnit deprecations.
This rehearsal uses synthetic historical data, not a production dump or production
volume. Repeat the audit and rehearsal on a sanitized copy of the target database
before an actual production deployment.
Two limitations remain explicit. An already authorized Reverb socket can continue
receiving after remote session revocation. Automatic business read/invalidation
calls for client-refresh are not yet wired. See
Broadcasting.
Both are explicitly deferred for this IAM delivery; frontend disconnection on
logout or 401 is not a guarantee of server-side WebSocket revocation.
The final review also reproduced an existing company authorization bug now shared
by client IAM. The administrator shortcut bypasses builtin-role/profile immutability
and own-assignment deletion/deactivation restrictions. Its correction is a
separate work item for this delivery, not a fix included in this branch.
See Builtin roles.