tey / concierge
A standard suite of essentials for every Laravel app you build.
Fund package maintenance!
Requires
- php: ~8.4.0||~8.5.0
- illuminate/contracts: ^13.0
- spatie/laravel-package-tools: ^1.93
- spatie/laravel-permission: ^8.3
- tey/mod: ^0.6.1
Requires (Dev)
- larastan/larastan: ^3.12
- laravel/fortify: ^1.41
- laravel/pint: ^1.27
- laravel/sanctum: ^4.3
- nunomaduro/collision: ^8.9
- orchestra/testbench: ^11.2
- pestphp/pest: ^5.2
- pestphp/pest-plugin-arch: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-typescript-transformer: ^3.3
- spatie/typescript-transformer: ^3.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.1
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-docs/roadmap-0.7
- dev-feat/update-user
- dev-feat/mod-0.6.1-frontend
- dev-feat/users-pages
- dev-feat/deactivation
- dev-chore/agents-md
- dev-feat/mod-0.6
- dev-feat/modes-kit-teams
- dev-feat/modes-app-wide
- dev-feat/modes-prep
- dev-docs/roadmap
- dev-spike/mod-host
- dev-feat/forced-password-reset
- dev-feat/initials-avatars
- dev-feat/registration-gating
- dev-refactor/drop-actions-data
- dev-docs/readme
- dev-chore/repo-conventions
- dev-docs/packagist-install
- dev-fix/local-login-process-environment
- dev-tenant-invitation-followups
- dev-tenant-invitations
This package is auto-updated.
Last update: 2026-10-11 23:11:30 UTC
README
Concierge: Front of House for Laravel
A standard suite of essentials for every Laravel app you build.
Concierge adds a standard suite of essentials and plumbing beyond Laravel's starter kits, so every app you build has a consistent foundation between laravel new and the product you're building.
Turn on local login, and your login page offers a one-click sign-in for local development:
// config/concierge.php 'local_login' => [ 'enabled' => true, 'account' => ['email' => 'dev@example.test', 'name' => 'Local developer'], ],
// your login page's props 'localLogin' => app(\Tey\Concierge\LocalAuth\LocalLogin::class)->offer(),
With LocalLoginButton on that page, it shows Sign in as dev@example.test. One click creates the account if needed and signs you in. Outside the local environment the offer is null, the button renders nothing and the route answers 404.
Note
Concierge is pre-1.0. Minor releases may change the API until 1.0.
- Membership: invitations, memberships, ownership and role grants scoped to a workspace
- Tenant invitations: a platform owner invites an email, and accepting gives that person their own new workspace
- Local login: one-click sign-in as a configured account, in the
localenvironment only - Vue pages: optional Vue 3 pages for members and invitations, and the local-login button
The full documentation is at concierge.teylabs.com. What is planned on the way to 1.0 is in ROADMAP.md.
Installation
Concierge requires PHP 8.4 or 8.5 and Laravel 13.
composer require tey/concierge
Composer also installs tey/mod, which concierge uses to write files into your app. Your app needs no setup for it and no config/mod.php: it gains the mod:* Artisan commands, and php artisan optimize caches mod's discovery.
The service provider is registered through package discovery. Publish the configuration to choose features and models:
php artisan vendor:publish --tag="concierge-config"
| Feature | Switch | Default |
|---|---|---|
| Membership | features.membership |
true |
| Tenant invitations | features.tenant_invitations |
false |
| Local login | local_login.enabled |
false |
The switches are read while the package provider registers, so set them in config/concierge.php, not at runtime. See Installation for the dependencies Composer brings in.
Quick Start
Membership is on by default, and its migration reads your user and workspace models, so set those up before you migrate:
-
Implement
Tey\Concierge\Contracts\Memberon your user model (with Spatie'sHasRoles) andTey\Concierge\Contracts\Workspaceon your workspace model. Both tables need incrementing integer keys. -
Publish the spatie/laravel-permission config and migrations, and set
permission.teamstotrue. -
Set workspace mode and the two models in
config/concierge.php:'mode' => 'workspaces', 'models' => [ 'user' => App\Models\User::class, 'workspace' => App\Models\Workspace::class, ],
-
Migrate, then create the configured roles:
php artisan migrate php artisan concierge:sync-roles
Then make someone the owner of a workspace:
use App\Models\User; use App\Models\Workspace; use Tey\Concierge\Actions\EstablishWorkspace; $owner = User::where('email', 'owner@example.com')->firstOrFail(); $workspace = Workspace::create(['name' => 'Acme']); EstablishWorkspace::run($workspace, $owner);
$owner now holds the owner role in that workspace and can invite others. Membership has the contract methods and the rest of the setup.
An app that only wants local login sets 'features' => ['membership' => false] first; then migrate needs no workspace or permission setup.
App-wide mode is for the app with one organisation: invitations and roles apply to the whole app, there is no workspace model or table, and Spatie teams stay off. Set 'mode' => 'app', leave models.workspace null, migrate and sync the roles as above, then make the first owner with EstablishOwner::run($owner). The actions are InviteUser, ChangeUserRoles and RemoveUser; AcceptInvitation and the concierge.member middleware work in both modes. Owner is the top role and several owners are allowed; granting it is how ownership passes on, and the last owner can never be removed or demoted. Your app's own "delete account" keeps working: deleting a user model removes their concierge row first, and the last owner is refused with a validation message on the password field. The Users pages and the upgrade to workspaces follow in 0.8 (#19).
Usage
Membership
Every operation is an action class in Tey\Concierge\Actions, called with ::run(): invite, resend, revoke and accept invitations; change roles; suspend, reinstate and remove members; transfer ownership; force a password reset. Actions check the grant rules in config/concierge.php and dispatch events. Your app keeps its routes, controllers, policies and authentication, and protects routes with the concierge.member middleware. See Membership.
DeactivateUser::run($actor, $user, reason: 'Left the company') takes a user's access away and keeps their history; RestoreUser::run($actor, $user) gives it back. The state lives on the user's concierge_accounts row, not on the users table, so your own queries are untouched: filter on status when you want to hide deactivated people. The same rules as removal apply through the AccountPolicy and AccountGuard: not yourself, not the last owner, not someone of equal or higher rank. Roles and, in workspace mode, memberships stay as they are, so restoring brings back exactly what the user had, with no new invitation. A listener on Laravel's Validated event refuses a deactivated user with "This account is deactivated." on the email key once their credentials check out, which covers Fortify, the starter kits and Auth::attempt. Add the concierge.active middleware after your authentication middleware (for example ['auth:sanctum', 'concierge.active']) to sign out a deactivated user on their next request; token requests get a 403 and the tokens are kept, so they work again after a restore. Deactivating also deletes the user's sessions when they live in the database driver and cycles the remember-me token; with other drivers the middleware ends the session. Workspace mode has no app-wide roles, so bind your own AccountPolicy to say who may deactivate. Listen to UserDeactivated and UserRestored.
ForcePasswordReset::run($actor, $membership, resetTwoFactor: true) locks an account down when it may be compromised: the password becomes one nobody knows, the remember-me token is cycled, two-factor is cleared when asked, and the person receives a reset link through your password broker. The same manager and rank rules apply as for suspending, and never to yourself. With the database session driver the person's sessions are deleted; with any other driver, add Laravel's AuthenticateSession middleware so their sessions end when the password hash changes. API tokens are untouched; listen to PasswordResetForced.
Tenant Invitations
For an invite-only app: a platform owner invites an email, and accepting creates that person's own workspace, which they own. It is off by default and ships no routes; your app provides the workspace factory, the link and the register-or-sign-in pages. See Tenant invitations.
Existing Teams
An app that already has a team or organisation model can use it as the workspace. Add implements Workspace and the ActsAsWorkspace trait (the name comes from a name column, and the workspace accepts members unless it is soft-deleted).
- A starter kit's teams (
laravel new --teams): set'workspaces' => ['engine' => 'kit']and pointmodels.workspaceat the kit'sTeam. Concierge reads each team's members and theirteam_members.role, resolves the current team from{current_team}orusers.current_team_id, and never writes the kit's tables. Spatie teams stays off. On the user model,HasRolesand the kit'sHasTeamsboth defineteams(), so keep the kit's:use HasRoles, HasTeams { HasTeams::teams insteadof HasRoles; }. Concierge's own write actions refuse a kit team for now (#57). - Any other model with a pivot stays on concierge's own engine (add
HasMemberstoo, with Spatie teams on). Copy its members in once withphp artisan concierge:import-members --pivot=organisation_user --role-column=role --dry-run --json. The command picks an owner, in this order: the role column's owner, the model'sowner_id, then the earliest member. It never changes the pivot, and running it again finds nothing to do.
Local Login
One click signs in as the account in your configuration, in the local environment only, checked on every request. It works without membership. See Local login for the exact rules, including cached configuration.
Vue Pages
Optional Vue 3 pages (MembersPage, InviteMemberPage, MemberPage, UsersPage, InviteUserPage, UserPage, AcceptInvitationPage) and LocalLoginButton ship in the package under resources/js. The Members pages are for workspaces; the Users pages are for app-wide mode. The PHP code never depends on them. In a Vue starter kit app, php artisan concierge:pages writes the four wrapper pages into your own resources/js/pages/Concierge, ready to edit (--dry-run previews them; mod lists the same recipe as mod:concierge/pages). Concierge declares the @tey/concierge alias, its TypeScript paths and its Tailwind source to mod, so php artisan mod:install inertia --dry-run shows the wiring and mod:install inertia applies it to a starter kit app. See Vue pages for the Vite, Tailwind and plugin setup by hand.
Users Pages (App-Wide Mode)
UsersPage lists everyone with search, a role filter and a status filter (Active by default, Deactivated, or both), paginated; set metadata.status to null or 'active' to hide deactivated users, 'deactivated' for only them and 'all' for everyone. UserPage edits a user's name, email and roles and links to removal, and InviteUserPage sends the app-wide invitation. Their props are UsersPageProps, UserPageProps and InviteUserPageProps in types.ts, and they never say "workspace". Concierge ships no routes yet (#28), so your controllers build the props and supply every link; a null link hides its control. Wire them to ChangeUserRoles, RemoveUser, InviteUser and UpdateUser. Changing an email does not clear email_verified_at or send a verification email; that is your app's call. Removal calls delete() on the user model, so a model with SoftDeletes is soft deleted. UserPage and the rows of UsersPage offer Deactivate (with an optional reason, sent as reason) and Restore when UserLinks carries a deactivate or restore link; both POST, so point them at routes that call DeactivateUser and RestoreUser. Your controller decides which users to list: filter on the account status to match the metadata.
UpdateUser::run($actor, $user, name: ..., email: ...) edits another user's name and email under the same rules as removal; your own profile stays with your app's profile page. A null argument leaves that field alone. The name is required (255 characters at most) and the email must be valid and unique on the users table, ignoring case, or a ValidationException keyed name or email is thrown. It writes the model's name and email attributes, which conciergeDisplayName() and conciergeEmail() read, and dispatches UserUpdated with the old and new value of each changed field.
To add your own columns, name them and return their values for each user, then put the same values on each UserRow:
// app/Providers/AppServiceProvider.php use Tey\Concierge\Facades\Concierge; Concierge::userColumns( ['plan' => 'Plan', 'seats' => 'Seats'], fn ($user) => ['plan' => $user->plan, 'seats' => $user->seats], ); // In the controller: send the labels in the metadata and the values on each row. new UsersMetadata(columns: Concierge::userColumnDefinitions(), /* ... */); new UserRow(columns: Concierge::userColumnValues($user), /* ... */);
Columns show after Joined. Render a cell yourself with the column slot, which receives column, user and value:
<UsersPage v-bind="props"> <template #column="{ column, value }"> <PlanBadge v-if="column.key === 'plan'" :plan="value" /> <template v-else>{{ value }}</template> </template> </UsersPage>
AcceptInvitationPage says "Join {app name}" when the invitation has no workspace; pass the name in metadata.appName, or leave it out for generic "Join" wording.
Every person on these pages has an avatar: the app's photo when it has one, and otherwise their initials on a colour that stays the same for that person everywhere. Nothing is fetched from an outside service. Build a UserSummary with UserSummary::for($user) and tell concierge where the photo lives:
// app/Providers/AppServiceProvider.php use Tey\Concierge\Facades\Concierge; Concierge::avatarUsing(fn ($user) => $user->profile_photo_url); // Jetstream
The Avatar component is exported for your own pages, and the eight --concierge-avatar-* colours are CSS variables you can override.
Configuration
The published config/concierge.php lists every key with a comment on each: the models, the roles and who may grant them, invitation expiry, tenant invitations and local login. Each feature page explains the keys it uses.
Who may register is one setting, CONCIERGE_REGISTRATION (open by default, domains, invitation or closed), enforced in Fortify's registration with no app code. See Registration.
FAQ
How Is This Different from spatie/laravel-permission, Starter-Kit Teams or an Admin Panel?
- spatie/laravel-permission is the roles and permissions engine, and concierge builds on it. Concierge adds what sits on top: invitations, ownership, rules for who may grant which role, suspension, and the events for each.
- Laravel's starter kits give an app sign-in, registration and, when generated with them, teams. Concierge picks up from there with membership administration as a package, so it updates with
composer update. The roadmap shows how it plans to work with each kit's own teams. - An admin panel gives you screens over your tables. Concierge gives you the flows themselves (invitation expiry, ownership transfer, grant rules), which an admin panel can call.
What Does My App Still Provide?
Routes, controllers and policies that call the actions; authentication; and your layout. Each feature page lists exactly what it expects.
Documentation
Everything else is at concierge.teylabs.com, with a compact index for coding agents at llms.txt.
Testing
composer test
composer analyse
composer lint
npm run types:check
npm run consumer:proof
Changelog
See CHANGELOG for what has changed recently.
Contributing
See CONTRIBUTING. Questions and ideas go to Discussions.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
- Scaffolded from spatie/package-skeleton-laravel
- Jasper Tey
- All Contributors
License
The MIT License (MIT). Please see License File for more information.