Follow the pattern every application already uses. A join table:
$this->schema->create('example_groups', function ($bp) {
$bp->id();
$bp->bigInt('example_id', true);
$bp->index('example_id');
$bp->bigInt('group_id', true);
$bp->unique(['example_id', 'group_id']);
});And the check, in your service:
public function maySee(array $row, array $viewerGroupIds): bool
{
$required = $row['group_ids'] ?? [];
return $required === [] || array_intersect($required, $viewerGroupIds) !== [];
}The viewer's groups are shared on every request: $this->app->make('template')->shared('viewerGroupIds', []).
🚨 In the service, not the template. Your content will be rendered on its own page, in a listing, in a widget, in search and possibly by a route that serves bytes — and a template check is one forgotten @if from a leak in any of them.
🚨 Add your table to Groups::RESTRICTION_TABLES. Miss it and deleting a group leaves your rows behind, which makes your content restricted to a group that does not exist — restricted to nobody, invisibly. Two tables were missing from that list for months.
🚨 Return 404, not 403, for something restricted. A 403 confirms it exists.