Migration

The $table->nestedSet() Blueprint macro adds the four maintained columns and a composite index that covers the common ancestor/descendant range lookups.

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('categories', function (Blueprint $table): void {
            $table->id();
            $table->string('name');
            $table->nestedSet();   // lft, rgt, parent_id (nullable), depth + index
            $table->softDeletes(); // optional — see Soft Deletes
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

For a scoped (multi-tree) table, pass the scope column(s) to the macro so each tree gets its own index slice — the scope columns are placed at the head of the same composite index, no separate index needed:

$table->nestedSet(scope: 'post_id');
// or for a multi-column scope:
$table->nestedSet(scope: ['tenant_id', 'post_id']);

cover: [...] appends columns to the tail of the index — useful when aggregate source columns benefit from covering range scans (see Filtered Aggregates).

To remove the columns later: $table->dropNestedSet() (pass the same scope / cover arguments you used in nestedSet()).

Non-integer primary keys

parent_id defaults to unsignedBigInteger to match Laravel's auto-incrementing id() column. UUID, ULID, and string PKs are supported — pass parentIdType: to match:

$table->nestedSet(parentIdType: 'uuid');

Accepted values: 'bigint' (default), 'uuid', 'ulid', 'string', or a closure function (Blueprint $table, string $column): void { … } for custom shapes (nanoid, fixed-width char, FK constraints, etc.). See Primary Keys for the monotonicity rule that governs chunked aggregate repair.