# Access Control List
ACL is how Krayin gates admin features by role. Each permission node is declared in a small PHP config file inside your package and merged into the core acl config from your service provider โ from then on, Krayin shows the matching checkboxes on the role-edit screen and your code can ask bouncer()->hasPermission('your.key') to gate UI or routes.
This page picks up from Admin Menu โ you'll usually add an ACL entry for every menu entry, so admins can hide things from users who shouldn't see them.
# ๐ Create the ACL config file
Inside your package's Config/ folder (the same one that holds menu.php), add acl.php:
packages
โโโ Webkul
โโโ Example
โโโ src
โโโ ...
โโโ Config
โโโ acl.php
โโโ menu.php
# Permission entry schema
Each array element describes a single permission:
| Key | Required | Purpose |
|---|---|---|
key | yes | Unique identifier. Use dot-notation to nest (examples.create, examples.delete). |
name | yes | Translation key for the label shown on the role-edit screen. |
route | yes | The named route this permission protects. |
sort | no | Display order on the role-edit screen. |
# Example
<?php
return [
[
'key' => 'examples',
'name' => 'example::app.acl.examples',
'route' => 'admin.examples.index',
'sort' => 2,
],
[
'key' => 'examples.create',
'name' => 'example::app.acl.create',
'route' => 'admin.examples.create',
'sort' => 1,
],
[
'key' => 'examples.delete',
'name' => 'example::app.acl.delete',
'route' => 'admin.examples.destroy',
'sort' => 3,
],
];
A top-level entry (examples) acts as the group; the dotted child entries (examples.create, examples.delete) become sub-permissions under it.
# โ๏ธ Merge the config into the service provider
Add a mergeConfigFrom() call inside register():
<?php
namespace Webkul\Example\Providers;
use Illuminate\Support\ServiceProvider;
class ExampleServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(
dirname(__DIR__) . '/Config/acl.php',
'acl',
);
}
}
Same rule as the menu config โ mergeConfigFrom() belongs in register(), not boot().
# โจ๏ธ Clear cached config
php artisan optimize:clear
# ๐ก๏ธ Gate code with bouncer()
Once your permissions are registered, use the bouncer() helper anywhere in your package to check them:
# In a controller
public function destroy(int $id)
{
if (! bouncer()->hasPermission('examples.delete')) {
abort(401, trans('admin::app.unauthorized'));
}
$this->exampleRepository->delete($id);
return response()->json([
'message' => trans('example::app.examples.delete-success'),
]);
}
# In a Blade view
Wrap UI in a permission check so unauthorised users don't see (or click) actions they can't perform:
@if (bouncer()->hasPermission('examples.create'))
<a href="{{ route('admin.examples.create') }}" class="primary-button">
@lang('example::app.examples.create-btn')
</a>
@endif
# ๐งช Verify
- Open Settings โ Roles in the admin and edit any role โ the new permissions should be listed under your group.
- Sign in as a user assigned to a role without
examples.delete. The Delete action should be hidden in the UI; if they try to call the route directly, they should get a 401.
If permissions don't appear on the role-edit screen, re-run php artisan optimize:clear and confirm mergeConfigFrom() is in register().
# ๐ Next steps
- Localization โ add the translation keys referenced by
namein youracl.phpentries. - Blade Components โ reusable UI primitives that already respect permission checks where relevant.
โ Blade Components Models โ