madewithlove/laravel-nova-uuid-support
Adds UUID primary key support to Laravel Nova, letting resources work with UUID-backed Eloquent models instead of integer IDs. Helps Nova fields, relationships, and search behave correctly when your app uses UUIDs.
This package is glue, not a framework. It exists because Nova assumes integer primary keys in several places — the ID field defaults to a number, BelongsTo/MorphTo searchable fields occasionally coerce keys, and route resolution expects a numeric resourceId. The package teaches those pieces to respect a model whose getKeyType() is string (UUID, ULID, or any opaque string).
Minimal install:
composer require madewithlove/laravel-nova-uuid-support
There is nothing to publish and no config file to touch. The day-to-day work is a trait on the model plus a one-line change in each resource.
First use case — a User model with a UUID id:
// app/Models/User.php
use Illuminate\Database\Eloquent\Concerns\HasUuids; // Laravel side: generates UUIDs
use Illuminate\Database\Eloquent\Model;
use Madewithlove\LaravelNovaUuidSupport\HasUuidPrimaryKey;
class User extends Model
{
use HasUuids;
use HasUuidPrimaryKey; // Nova side: tells Nova how to read/route the key
}
// app/Nova/UserResource.php
public function fields(NovaRequest $request): array
{
return [
ID::make()->asText()->sortable(), // critical: keep it a string
Text::make('Name')->sortable(),
// ...
];
}
Smoke test, in this order: index page (does the ID render fully?), detail page (does the route resolve?), create (does it save?), a BelongsTo picker (does search find records?), global search, then attach/detach on a BelongsToMany.
If your Nova version prefers the trait on the resource instead of the model, the README is the source of truth — verify against the src/ directory before wiring it into more than one resource.
One trait per UUID model, not blanket-applied. Only add the trait to models whose $keyType is string and $incrementing is false. In a mixed app (users UUID, posts auto-increment), this separation is what keeps Nova's intval/Number path intact for integer models.
Relationships carry the key type through the foreign key, not the resource. A BelongsTo::make('Owner', 'owner', UserResource::class)->searchable() works because the related model's key type is string; the parent resource's key type is irrelevant. This means you can have an integer-keyed Post that BelongsTo a UUID-keyed User with no special configuration — just make sure both resources have consistent searchableColumns.
Global search needs explicit columns. $search on the model is a list of column names, and Nova will where like across them regardless of type. Keep id out of $search — lexicographic UUID matches are useless and confusing in the search dropdown.
Route binding is the quiet dependency. Nova resolves {resource}/{resourceId} through resolveRouteBinding, so keep getRouteKeyName() returning 'id'. If you see 404s on detail pages, check for a global Route::pattern('resourceId', '[0-9]+') or a Resource::resolveRouteBinding override in a base resource class.
Actions, Lenses, and Metrics operate on getKey() and generally work unchanged — the failure mode is your own action code casting to int ((int) $model->getKey()). Audit custom actions before shipping.
MorphTo is where things bite. A MorphTo that can point at both an integer-keyed and a UUID-keyed model will render fine but can misbehave in searchable mode. Test each morph target explicitly; if it's a problem, split into two MorphTo fields with ->types([...]) restricted to UUID models.
Testing. Use Laravel's HasUuids on the model and factories; Nova's resource classes are mostly stateless helpers, so a feature test that hits /nova-api/users and asserts a 200 with the UUID in the response body is enough to catch regressions on Nova upgrades.
Pin and re-test on every Nova upgrade. The last release was 2024-09-16. Nova 4 → 5 changed several field internals; the package patches around them, so a Nova bump is a real regression risk. Add it to your upgrade checklist — "does the UUID resource index/detail still load?"
ID::make() alone is not enough. Without ->asText() the field renders through Number, and long UUIDs truncate or display as 0. Use ->asText(), or swap in a plain Text::make('ID', 'id')->sortable() if you want to avoid the ID field entirely.
Sorting by a UUID is lexicographic, not chronological. Your default $orderBy on the resource should be created_at (or a sequence/timestamp column), not id. Otherwise pagination looks random to users.
Verify the trait actually applied. If relationships misbehave, run in Tinker:
$u = new App\Models\User;
dump($u->getKeyType(), $u->getIncrementing(), $u->getKeyName());
Expected: "string", false, "id". If getIncrementing() is still true, the UUID trait and the package trait are fighting — check model $incrementing and $keyType properties too, they take precedence over traits in some combinations.
BelongsTo "show create" button. The inline create modal for a related resource works, but the created record's ID must be returned in the response. If the package is misapplied, you'll see "record created" but the field stays blank — that's the symptom of a string key being dropped from the JSON payload.
Pivot actions and Detach rely on IDs round-tripping as strings. If Detach sends empty resources, check that your BelongsToMany field isn't using ->fields(fn () => [ID::make()->asNumber()]) inside a pivot block.
Don't stack UUID packages. If you're on Laravel 9+, HasUuids handles generation; this package only handles Nova-side presentation and resolution. Pulling in webpatser/laravel-uuid or a third Nova UUID plugin on top tends to produce conflicting resolveUsing callbacks.
Extension point — resource-level override. If you need finer control than the trait provides, override searchableColumns() on the Nova resource (returning ['id', 'name', 'email'] explicitly) rather than relying on Nova's inference. That's the lowest-friction escape hatch when the package lags behind a Nova release.
Queues and caches are unaffected. SerializesModels serializes $id as whatever type it is, and cache keys built from UUIDs work fine — no special handling needed.
Debugging checklist when a UUID resource breaks after a deploy: (1) php artisan nova:publish — no; (2) php artisan optimize:clear — Nova caches resource discovery; (3) confirm the trait is in the use block and the class actually implements it (IDE auto-imports occasionally alias the wrong class); (4) Log::info((new UserResource($user))->serializeForIndex(new NovaRequest)) to see what the API is emitting.
How can I help you explore Laravel packages today?