tey/concierge
Tey Concierge is a lightweight Laravel package that adds a clean, configurable concierge layer to your application. It centralizes common request-handling logic, keeps controllers slim, follows Laravel conventions, and offers sensible defaults with an expressive API, making it simple to install, con...
composer require tey/concierge
php artisan vendor:publish --provider="Tey\Concierge\ConciergeServiceProvider"
php artisan migrate # v0.8.1 includes update_concierge_invitations_with_acceptor
php artisan list | grep concierge
Before wiring anything into an app, read these in order:
src/ConciergeServiceProvider.php — the contract. It lists every trait, macro, helper, singleton, command, migration, and config merge the package registers.config/concierge.php — most behaviour is gated by toggles. For v0.8.1 specifically, review concierge.invitations.match_email (default true, preserving existing behaviour).docs/scenarios.md — v0.8.1 adds the join-as invitation cases and updates the accept decision tree. This is the fastest way to understand the intended invitation flow.composer.json → extra.laravel.providers — confirm auto-discovery. If absent, register the provider manually in bootstrap/providers.php.First real use case: install on a fresh internal app and exercise the invitation acceptance flow end-to-end. v0.8.1 adds a meaningful branch: accepting as the signed-in account when their verified email differs from the invited email. Test both match_email: true and match_email: false before rolling it into a client-facing system.
Configure invitation matching deliberately. Leave concierge.invitations.match_email at true unless you explicitly want to allow join-as behaviour. Setting it to false allows a signed-in user with a different, verified email to accept as themselves, but every other check still applies: expiry, revoked status, workspace accepting members, inviter authorisation, grants unchanged, and already-a-member checks.
Pass the join-as state into the accept UI. When rendering the invitation accept page's ready state, provide the additional values so the UI can offer “Join as {email}” before “Switch account”:
return view('concierge::invitations.accept', [
// existing state...
'otherEmail' => $inspection->otherEmail,
'signedInAs' => auth()->user(),
'switchAccount' => route('concierge.invitations.switch-account'),
]);
If you do not pass these, the default UI will not offer the join-as path even when match_email is false.
Surface the acceptance identity in member lists. MembersIndex rows carry acceptedWithOtherEmail and invitedEmail in v0.8.1. Use these to show hosts who accepted via a different account without guessing from the current user record.
Adopt incrementally by config, not by import. Publish the config on day one and flip features on as needed. Keep the invitation toggle explicit in your own config or documentation so behaviour is explainable.
Prefer traits over inheritance. Essentials packages usually ship Concerns\* traits. Compose them:
class Order extends Model
{
use \Tey\Concierge\Concerns\HasConciergeDefaults;
}
Keep your own base class in between if you already have one — do not let the package dictate your model hierarchy.
Resolve through the container, not globals. If the package ships both helper functions and a facade/binding, use the binding inside application code so tests can swap it:
public function __construct(private \Tey\Concierge\Concierge $concierge) {}
Reserve global helpers for Blade templates and one-off scripts.
Register macros in a dedicated provider. If the suite adds Collection/Stringable macros, keep all macro registration in one MacroServiceProvider.
Isolate it from distributable packages. Never depend on tey/concierge from a package you publish on Packagist. Use it in applications only; if you need the behaviour inside a package, copy only the lines you need.
Testing. For invitation changes, assert both config states: default match_email: true rejects a different email, and false accepts only when the other checks pass. Assert that the invitation records the acceptor and acceptedWithOtherEmail. For application tests, assert on response shape and model attributes rather than package internals.
Run the v0.8.1 migration. Publish and run update_concierge_invitations_with_acceptor before deploying code that reads acceptedWithOtherEmail. It adds one boolean column defaulting to false. Skipping it will produce missing-column errors in member views or invitation records.
match_email defaults to true. This is today's behaviour. Only set it to false when you want to offer join-as. If you set it to false but forget to pass otherEmail, signedInAs, and the switchAccount link on the ready state, the UI will not offer the new path.
Join-as is not a bypass. It still requires a different, verified email and all existing checks: expiry, revoked, workspace accepting members, inviter still authorised, grants unchanged, and not already a member. Do not treat it as an admin override.
Config caching. Publish, edit, then php artisan config:cache. Forgetting the rebuild after changing config/concierge.php is the classic “my toggle does nothing” bug. In cached mode, env() inside package config will return null.
Pin the version. Use "tey/concierge": "0.8.*" or an exact tag. With zero dependents there is no ecosystem pressure to keep semver honest. Read CHANGELOG before every bump, especially around migrations and invitation behaviour.
Global helper collisions are still the most likely failure mode. Helper functions live in the root namespace; a second package defining similar names can cause a fatal “cannot redeclare” at boot. Audit for overlap with laravel/helpers or similar.
Trait method collisions. A trait shipping booted(), getAttribute(), or a scope* can silently lose to another trait or your own model. Test models that combine it with SoftDeletes, HasUuids, or auditing packages.
Auto-discovery surprises. If the provider is discovered and also registered manually, you get double registration: duplicated macros, doubled middleware, or “macro already exists”. Check php artisan about and bootstrap/providers.php before debugging anything else.
Debug the container when something is missing.
php artisan tinker
>>> app()->getBindings(); // is the singleton registered?
>>> Str::hasMacro('foo'); // did the macro land?
If a command is not in artisan list, the provider is not loading — start there, not at the command class.
Extension points to look for. Most suites of this shape expose a config array of class-strings, an interface bound as a singleton you can bind() over, or an extend()-style hook. Rebinding the interface in your own provider is the cleanest customization; editing vendor/ is not an option.
Plan an exit. With one maintainer and no dependents, the bus factor is real. Keep the surface area small — a handful of traits, helpers, and invitation views is a manageable rewrite; a deeply integrated suite is a migration project. Document which package features you actually rely on.
How can I help you explore Laravel packages today?