Migrations
Define database migrations for your extension. They live under server/migrations/, follow Fleetbase's UUID + public_id conventions, and load automatically via the service provider.
Migrations
Migrations define the schema your extension owns. Fleetbase auto-loads them via your service provider ($this->loadMigrationsFrom(__DIR__ . '/../../migrations')), so they run alongside core migrations whenever someone runs php artisan migrate.
Where Migrations Live
server/
└── migrations/
└── 2026_01_15_000000_create_widgets_table.phpNote: it's a flat migrations/ directory at server/migrations/, not server/database/migrations/. The path matches the standard Laravel package convention used across core-api and every shipped extension.
Naming Convention
Standard Laravel migration filenames: YYYY_MM_DD_HHMMSS_<verb>_<table_name>_table.php. Examples:
2026_01_15_103000_create_widgets_table.php2026_01_18_142000_add_status_to_widgets_table.php2026_01_22_080000_add_foreign_keys_to_widgets_table.php
Foreign keys go in a separate later migration so the parent tables already exist when the constraint is added — see how pallet/server/migrations/ and storefront/server/migrations/ split creation and FK migrations into pairs.
Table-name Convention
Prefix your tables with your extension's namespace to avoid collisions:
Schema::create('my_extension_widgets', function (Blueprint $table) { ... });Don't reuse a name that core uses (users, companies, orders, transactions, etc.). When in doubt, scope it.
Standard Column Pattern
Fleetbase models follow a consistent shape — copy this skeleton for any organization-scoped resource:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('my_extension_widgets', function (Blueprint $table) {
$table->increments('id');
$table->char('uuid', 36)->nullable()->unique(); // primary identifier
$table->string('public_id')->nullable()->unique(); // user-visible ID (e.g. "widget_a1b2c3d4")
$table->char('company_uuid', 36)->nullable()->index(); // org scoping — required for shared infra
$table->char('created_by_uuid', 36)->nullable()->index();
$table->string('name');
$table->text('description')->nullable();
$table->json('meta')->nullable();
$table->string('status')->default('active');
$table->softDeletes(); // adds deleted_at
$table->timestamps(); // adds created_at, updated_at
});
}
public function down(): void
{
Schema::dropIfExists('my_extension_widgets');
}
};| Column | Why |
|---|---|
uuid (char(36)) | Internal stable identifier — referenced by other tables via *_uuid foreign keys |
public_id | Human-friendly ID surfaced via the API. Generated by HasPublicId trait on the model |
company_uuid | Multi-tenant scope. Every per-org table needs this, indexed |
created_by_uuid | Audit trail back to the users table |
meta (json) | Catch-all for extension-specific fields you don't want columns for |
softDeletes() | Standard across Fleetbase models — mirrors trash/restore |
timestamps() | Always |
Foreign Keys
Add foreign keys in a separate migration that runs after the create migration:
return new class extends Migration {
public function up(): void
{
Schema::table('my_extension_widgets', function (Blueprint $table) {
$table->foreign('company_uuid')
->references('uuid')->on('companies')
->cascadeOnDelete();
$table->foreign('created_by_uuid')
->references('uuid')->on('users')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('my_extension_widgets', function (Blueprint $table) {
$table->dropForeign(['company_uuid']);
$table->dropForeign(['created_by_uuid']);
});
}
};Reference uuid (the FB convention), not the integer id.
Running Migrations
In a development extension you'll typically run:
docker compose exec application php artisan migrateIf you've made schema changes during local iteration:
docker compose exec application php artisan migrate:fresh --seedThe console exposes a re-runnable scaffold via the flb CLI — flb scaffold generates a starter migration when you create a new model.
Models
Models live under server/src/Models/. Extend Fleetbase\Models\Model (not Laravel's base Model) — the base wires in UUID generation, public-id generation, soft-deletes, JSON-cast helpers, and the activity log.
namespace MyOrg\MyExtension\Models;
use Fleetbase\Models\Model;
use Fleetbase\Traits\HasPublicId;
use Fleetbase\Traits\HasUuid;
use Fleetbase\Traits\TracksApiCredential;
class Widget extends Model
{
use HasUuid;
use HasPublicId;
use TracksApiCredential;
protected $table = 'my_extension_widgets';
protected $publicIdType = 'widget';
protected $fillable = ['name', 'description', 'meta', 'status', 'company_uuid', 'created_by_uuid'];
protected $casts = [
'meta' => 'array',
];
}$publicIdType is the prefix of the generated public_id (e.g. widget_a1b2c3d4). Browse traits at packages/core-api/src/Traits.
Sandbox & Multi-tenant Considerations
Every Fleetbase install runs two databases — production and sandbox — with the same schema. The MigrateSandbox console command handles syncing migrations across both. By default your migrations run against both; if you have a migration that should only run in production, set the extension flag in composer.json:
"extra": {
"fleetbase": {
"sandbox-migrations": false
}
}Most extensions don't need this flag — by default migrations run against both production and sandbox, which is what you want for multi-tenant data.
Source
| File | Description |
|---|---|
core-api/migrations/ | Core schema migrations — reference shapes |
pallet/server/migrations/ | Reference extension migrations |
storefront/server/migrations/ | Reference extension migrations |
src/Console/Commands/MigrateSandbox.php | Sandbox-aware migration runner |
src/Traits/HasUuid.php | UUID auto-generation trait |
src/Traits/HasPublicId.php | public_id generation trait |