Add a <thing>_groups table, check the viewer's group ids against it in your service, and add the table to Groups::RESTRICTION_TABLES. Enforce it in the service, never in a template.

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.

Sign in to say whether this helped.