Loading...

This is taking longer than expected.

Build a team-scoped CRUD

The resource base class that keeps a Laravel Front CRUD inside the team, and how to register its route and permissions.

Weblabor Base builds its admin screens with Laravel Front: a resource class declares the fields and the kit renders the list, the forms and the detail page. See The admin panel on the base's site. Inside a team the same tool works, with one base class that keeps every record and every permission check inside the team. This guide shows it.

The base class

app/Front/Resources/Team/Resource.php:

namespace App\Front\Resources\Team;

use App\Front\Resources\Resource as Base;
use Illuminate\Database\Eloquent\Model;

class Resource extends Base
{
    public $guards = ['team'];
    public $layout = 'layouts.teams';
    public $section = 'team';

    public function indexQuery($query)
    {
        return $query->where('team_id', team()->id)->latest();
    }

    public function processDataBeforeSaving($data)
    {
        $data['team_id'] = team()->id;
        return $data;
    }

    public function mediaOwner(): ?Model
    {
        return team();
    }
}
What it sets What it means
$guards = ['team'] The retrieve, create, update and delete permissions of the resource are created on the team guard and checked against the team's roles.
$layout = 'layouts.teams' The screens render inside the team workspace, with its sidebar.
$section = 'team' The resource is listed in the team workspace's sidebar, and not in the admin panel's.
indexQuery The list shows only the current team's records.
processDataBeforeSaving A record created or updated from the form belongs to the current team, whatever the request carried.
mediaOwner An image or file field of the resource uploads into the team's file library and counts against the team's space, not the member's.

A resource that extends the base's App\Front\Resources\Resource instead is wrong in three ways at once: its permissions land on the web guard, so a member with the right team role is refused; its list shows every team's records; and its records are saved with no team_id.

Writing one

  1. The table needs a team_id column, indexed, pointing at teams.
  2. Create the class under app/Front/Resources/Team/:
namespace App\Front\Resources\Team;

use App\Models\Project as Model;
use WeblaborMx\Front\Inputs;

class Project extends Resource
{
    public $base_url = '/team/projects';
    public $model = Model::class;
    public $icon = 'folder';

    public function fields()
    {
        return [
            Inputs\ID::make(),
            Inputs\Text::make('Name')->rules('required'),
            Inputs\Textarea::make('Description')->hideFromIndex(),
        ];
    }
}
  1. Register it in routes/team.php. That file already runs inside the team group, with the auth, security and team middleware, so the team middleware sets the context before any check runs, and the line needs no group of its own:
// routes/team.php
Route::front('Team\\Project');

The group asks for no subscription. Your screen is protected by its own permissions, the ones the next step seeds. Requiring a subscription is your decision: when the screen has to stay closed to a team without an active plan, check it in the screen itself, for example with team()->subscription('default')?->active(). Do not use ensure.subscribed:team for it, because it also asks for manage plans and shuts out every member without that permission.

Laravel Front names the routes team.front.projects, team.front.projects.create and so on.

  1. Seed the permissions:
php artisan db:seed --class=PermissionSeeder

With discover_front_permissions on in config/app.php, the seeder finds the resource and creates retrieve project, create project, update project and delete project on the team guard: the singular of the model's table name. The seeder only creates permissions; it gives them to no role. A team created from then on gets them in its Administrator role, because TeamObserver::created() gives that role every team permission except those listed in team_restricted_permissions. The teams that already exist do not: write a migration that gives the new permissions to every role named team_owner_role on the team guard, as 2026_10_14_100001_create_the_manage_media_permission_of_the_team_guard.php does for manage media. Every other member gets them through a role. See Team roles and permissions.

  1. The sidebar needs no edit. Because of $section = 'team', the team workspace's sidebar lists the resource, under its plural label, for every member whose role passes the policy's viewAny. Set public $menu_group = 'Team'; to place it in the Team group, next to Members and Roles, or 'Billing', 'Files' or 'Administration' for one of the other groups; without it the link sits at the top, next to Dashboard. The hand-written links, such as members, settings and media, are in resources/views/components/weblabor-teams/layouts/sidebars/team.blade.php.

The policy

Laravel Front asks the model's policy. Extend App\Policies\BasePolicy and name the permission:

namespace App\Policies;

class ProjectPolicy extends BasePolicy
{
    protected string $name = 'project';
}

The property is protected string $name, as BasePolicy declares it: a public $name is a fatal error when the class loads. The name is the second word of the permissions the seeder created, project here.

BasePolicy checks the right guard from the context and refuses a record of another team on view, update and delete. restore and forceDelete also ask that the person hold the team's owner role (team_owner_role). Two optional properties close the whole policy: protected $requiredFeature = 'projects'; refuses every check while the current team lacks that capability, and protected $requiredConfig = 'features.projects'; refuses it while that config value is off. Override a method only for a rule of your own, and call the parent first so the team checks stay.

The one in the kit

App\Front\Resources\Team\Role, at /team/roles, is the team's roles CRUD and the example to copy for the scoping: it extends the admin panel's App\Front\Resources\Role, not the base class above, and sets the same $guards, $layout and $section itself, scopes its list with indexQuery, writes team_id and the team guard in processDataBeforeSaving, sets $menu_group = 'Team', and adds the clone action. Its route is registered the same way, with Route::front('Team\\Role').

Back