lake/larke-auth
Larke Auth is a Laravel/PHP package that provides a lightweight authentication layer for your applications, with helpers and configuration to integrate login, registration, and access control into your project while keeping auth setup simple and customizable.
lake/larke-auth is a small, single-maintainer authorization package aimed at the same problem space as spatie/laravel-permission (ACL/RBAC) plus rule-driven checks (ABAC). With 1 star and 0 dependents, expect to read the source rather than the docs.
Install
composer require lake/larke-auth
Laravel's package discovery should register the service provider; if not, add it manually to config/app.php (providers and an Auth/LarkeAuth alias). Confirm the exact class name by looking at composer.json → extra.laravel.providers in the vendor directory.
Publish and migrate
php artisan vendor:publish --provider="Lake\\Auth\\AuthServiceProvider"
php artisan migrate
You should end up with tables for roles, permissions/rules and their pivot tables. If the publish tag differs, list tags first:
php artisan vendor:publish --list
Wire the trait onto your user model
use Lake\Auth\Traits\HasAuth; // confirm namespace in vendor/lake/larke-auth/src
class User extends Authenticatable
{
use HasAuth;
}
First use case — protect a route with a permission
Route::get('/reports', ReportController::class)
->middleware(['auth', 'can:reports.view']);
Seed a permission and attach it to a role, then give the role to a user. Then verify the whole chain resolves:
$user->assignRole('editor');
$user->givePermissionTo('reports.view'); // if direct ACL assignment is supported
Gate::forUser($user)->allows('reports.view'); // => true
Where to look first: src/ for the trait and the Gate/Auth integration, config/ for the guard and model bindings, and the migrations for the actual table/column names — those three files tell you 90% of the API.
RBAC as the default spine. Roles are containers, permissions are the atoms, users get roles. Keep permission names as dotted strings (reports.view, orders.refund) and treat them as constants — define them in a seeder or an enum so typos fail at boot rather than in production.
// database/seeders/PermissionSeeder.php
foreach (PermissionEnum::cases() as $perm) {
Permission::firstOrCreate(['name' => $perm->value]);
}
ACL for the exceptions. Direct user→permission grants are useful for one-off access ("give Alice billing export for the quarter") without polluting the role model. Use them sparingly and audit them — they're invisible when you read a role definition.
ABAC for anything conditional. Pure RBAC breaks down on rules like "managers can approve expenses under $5,000 in their own department." Encode these as rule objects or closures rather than exploding your permission list:
Gate::define('expense.approve', function (User $user, Expense $expense) {
return $user->hasPermissionTo('expense.approve')
&& $expense->amount <= $user->approval_limit
&& $expense->department_id === $user->department_id;
});
This is the pattern that ages best: the package answers "does the user hold the capability," your Gate closure answers "does it apply here."
Bridge to Laravel Gates/Policies, don't fight them. Register Gate::before only for genuine super-admins; otherwise let the package's can() support feed Laravel's native Gate, so @can, ->can(), authorize() and middleware all work without a second mental model.
// AppServiceProvider::boot()
Gate::before(fn (User $user) => $user->hasRole('super-admin') ?: null);
Return null (not false) so normal checks still run.
Middleware layering. Combine a coarse route-level check (can:reports.view) with a fine-grained policy check in the controller. Route middleware is the cheap gate; the policy is the authoritative one.
Testing. Write a small factory helper — User::factory()->withRole('editor') — and assert both directions (allowed and denied). Authorization bugs are almost always over-permission, and tests that only assert the happy path never catch them.
Config-driven model swapping. If the config exposes role/permission model bindings, subclass the package models in app/Models and point the config at yours. That's the cleanest extension point for adding columns or relations without forking.
composer show lake/larke-auth and ls vendor/lake/larke-auth/src before writing code. Budget an hour for source reading.config/ and a forgetCachedPermissions()-style method. php artisan cache:clear is your first debugging step, not your last.admin), permission checks can silently return false because the package resolves the user from the default guard. Set the guard explicitly in config and confirm with Gate::forUser($admin)->allows(...).users already exists. If you're on an existing database with custom user tables, publish the migrations, rename the foreign keys, and run them in isolation before the package's pivot tables.spatie/laravel-permission. Both define HasRoles, Role, Permission and assignRole. Never install both. If you're migrating, map Spatie's guard_name column — this package may not have an equivalent, which changes how multi-guard setups behave.firstOrCreate/updateOrCreate in permission seeders; create will fail on the second deploy and block your release pipeline.DB::listen() while calling can() shows you exactly which tables are consulted — the fastest way to learn an undocumented package's resolution order (direct permissions vs. role permissions vs. rules).composer.json ("lake/larke-auth": "^1.0" or the exact tag) and be prepared to vendor/fork it. Wrap your calls behind your own AuthorizationService or Gate definitions so swapping in Spatie later is a one-file change rather than a codebase-wide refactor.Gate::before/after hooks the package registers by default — an implicit before is the most common cause of "everything is allowed for some users."How can I help you explore Laravel packages today?