hasinhayder/tyro
Tyro is a lightweight Laravel package by Hasin Hayder for role and permission management. It lets you assign roles and permissions to users, protect routes with middleware, and use simple Blade directives—all through a clean, familiar Laravel API.
Install and publish. Pull the package with Composer, then publish its config, migrations, and any seeders in one pass. Tyro is not auto-discovered-and-forget — the config file (config/tyro.php) is the single most important artifact, because it drives table names, model bindings, cache keys, and middleware aliases. Read it top to bottom before writing a single authorization check.
Wire the user model. Add the Tyro trait (the package ships one for HasinHayder\Tyro contracts) to your App\Models\User, or point the config at a custom user model if you have one. This is the step people skip and then spend an hour debugging why $user->roles is empty.
Run the CLI installer. Tyro's headline feature is its Artisan surface, following a tyro:{entity}-{action}, tyro:sys-*, and tyro:auth-* convention. Run the sys-level install/setup command first — it typically seeds system roles, system privileges, and the break-glass super-admin path. Do this in local and staging before touching production.
Look here first, in order:
config/tyro.php — table names, model classes, cache TTL, middleware aliases, tenant settings.php artisan list tyro — the CLI is the documentation. Every entity/action pair you need is discoverable.vendor/hasinhayder/tyro/src/ — the middleware and Blade directives are thin wrappers; reading them tells you the exact resolution order Tyro uses for a permission check.First real use case. Create a role via the CLI (tyro:role-create or equivalent), attach a permission to it, assign it to a user, then protect one route with the package's role middleware and gate one Blade block with its directive. Doing this end-to-end in ten minutes is a better orientation than reading the README, because it forces you through publish → migrate → seed → trait → middleware → cache, which is exactly the path every subsequent feature takes.
Route-level middleware for coarse gates, policies for fine-grained ones. Use Tyro's role/permission middleware on route groups — it's fast, declarative, and cached. Reserve Laravel policies for object-level decisions ("can this user edit this post") and delegate the role/permission portion of the policy to Tyro rather than reimplementing it. Mixing the two layers is normal; duplicating the check logic is not.
Blade directives for UI affordances, never as the security boundary. The directives are for hiding nav items and buttons. Every action they guard must also be enforced server-side by middleware or a policy, because a Blade conditional is trivially bypassed by anyone with an HTTP client.
Provision roles and permissions through Artisan, not through tinker or one-off scripts. Because the CLI is the package's core abstraction, put role creation, permission attachment, and default grants into a deploy step or a seeder that calls the same tyro:* commands. This keeps production and local environments identical and gives you a re-runnable audit trail of what changed. Idempotent commands are your friend here — re-running a seeder in CI should not duplicate roles.
Treat permissions as code, privileges as escape hatches. The package distinguishes between assignable permissions and system-level privileges. Model your domain permissions (post.publish, invoice.refund) as ordinary permissions attached to roles, and keep tyro:sys-* privileges for operational break-glass scenarios (super-admin, impersonation, cache flush). Don't hand system privileges to application roles casually — they bypass the normal resolution pipeline.
Suspensions are a first-class state, not a soft delete. Tyro models suspensions at the user, role, permission, and tenant level. Use them for temporary lockouts and compliance holds instead of flipping is_active flags on your own tables. When a user is suspended, decide whether your middleware should 403 or redirect — the package will resolve suspensions for you, but the UX is your responsibility.
Tenant scoping belongs in the middleware or a context setter, before any check runs. If you use multi-tenancy, the tenant context must be established on every request before authorization resolves. A permission checked without tenant context will fall back to the global scope, which is almost never what you want and is a subtle privilege-leak vector.
Consume the audit trail. Tyro emits dot-notation events ({entity}.{action}) and keeps an immutable log. Wire a listener to ship those events to your SIEM, Sentry breadcrumbs, or an internal admin feed. This is the cheapest compliance win in the package and most teams never turn it on.
Extension points to know: configurable model classes (swap in your own Role extending Tyro's), configurable table names (for legacy schemas), and custom permission resolvers/providers if your org has a hierarchical source of truth (LDAP, an external IAM). Prefer these over forking.
Cache invalidation is the #1 footgun. Permission and role resolution is cached under keys like tyro:user-{id}:roles and tyro:user-{id}:privileges. If you mutate roles directly via Eloquent (rather than through the package's API), the cache won't bust and you'll get "I gave them the role but they still can't see it." Always mutate through Tyro's service/methods, or explicitly flush the user's cache keys afterward. In long-running workers (Horizon, Octane), this bites hardest — consider a warm/flush strategy on deploy.
Clear the cache in tests. Between test cases, stale authorization cache can make a passing suite fail mysteriously. Flush Tyro's keys in a setUp hook or use the package's testing helpers if provided. Combine with RefreshDatabase and you get deterministic runs.
Config table names and model bindings must move together. If you override the roles table but not the model's $table, or vice versa, you'll get empty relationship collections rather than an obvious error. Treat config/tyro.php changes as migration-level changes.
Published migrations are point-in-time. When you upgrade Tyro, new migrations may land in the package. Re-publish with the --force flag carefully, or diff against what you already have — overwriting a published migration in production is a bad afternoon.
Wildcard permissions and resolution order matter. The package supports inheritance and wildcard grants. When a check seems wrong, the resolution pipeline is the thing to reason about: direct permission → role permission → inherited role → privilege → suspension override. Log the resolved set for a user (the cache key contents) before assuming a bug.
Sanctum coupling. Tyro is built on Sanctum, so its token/auth assumptions align with Sanctum's. If you're using session auth or Passport alongside it, verify that the middleware ordering in bootstrap/app.php (Laravel 11+/12 style) places Tyro's middleware after authentication — a role check on an unauthenticated request should 401, not 500.
Middleware aliases are registered by the package, but the ordering is yours. In the new Laravel application bootstrap, register Tyro's aliases and, more importantly, order them relative to auth, verified, and any tenant-resolution middleware. Wrong order = wrong user = wrong permissions.
Break-glass and super-admin paths are load-bearing — test them. Every codebase eventually needs "how do I get back in when the roles table is wrong." Verify the sys-level recovery command works in staging, because it will be the one command you run under pressure.
Debug with the CLI before the browser. When a permission check fails, the fastest path is usually the Artisan diagnostic commands — inspect the user's resolved roles/privileges directly from the console, bypassing the HTTP stack and the Blade cache. It takes thirty seconds and saves an hour of dd().
Don't authorize in loops. Calling $user->can(...) inside a foreach over a hundred models will hammer the cache and DB. Eager-load roles/permissions, or pre-resolve the permission set once and compare in PHP for bulk operations.
How can I help you explore Laravel packages today?