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

  1. KSUID primary key — $table->string('id')->primary()
  2. Tenant scoping — company_id and/or organization_id with indexes
  3. Timestamps — $table->timestamps()
  4. Indexes — On foreign keys and commonly filtered columns

Foreign keys

Running migrations

PostgreSQL-specific features

Stored procedures

Custom PostgreSQL procedures live in database/procedures/:
  • cashing_range_list.pgsql — Generates cashing reports by date range

JSON columns

PostgreSQL’s JSONB type is used for flexible data:
Query JSON columns:

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 in iam_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.
Historical company agents without a profile remain valid and migratable. Ownerless refresh rows are not valid under the new XOR constraint. Builtin role templates with null 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, especially client_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:
Keep the existing 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 commit 4efc9f9f. 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.