liberusoftware/jetstream-bridge
Integration bridge that connects Liberu Software's modular Laravel packages with Jetstream, wiring authentication, teams, profile management and API tokens into one stack. Ships service providers, config and adapters so Jetstream features work consistently across Liberu components.
The bridge sits between your Laravel app's auth scaffolding (Fortify + Jetstream) and Liberu's identity layer. It ships adapter classes that implement the Fortify action contracts (create user, update profile, update password, reset password, 2FA enable/confirm/disable, email verification) while delegating the actual persistence and business rules to Liberu identity services. In practice: you keep Jetstream's UI and Fortify's routes, and swap the implementation underneath.
Minimal path:
composer require liberusoftware/jetstream-bridge
php artisan vendor:publish --provider="Liberu\JetstreamBridge\JetstreamBridgeServiceProvider"
// src/JetstreamBridgeServiceProvider.php
$this->app->bind(CreatesNewUsers::class, LiberuCreateNewUser::class);
Jetstream::useUserModel(config('jetstream-bridge.models.user'));
Whatever it binds tells you the extension points. Open src/Actions (or src/Adapters) next — those classes are what you'll subclass when identity rules differ.
Cross-check your configs. Open config/fortify.php and confirm 'actions' => [...] points at the bridge's classes rather than the generated App\Actions\Fortify\* defaults. If the package publishes config/jetstream-bridge.php, check the features, model, and stack keys match what your app already uses (Livewire vs. Inertia changes nothing here, but two_factor_authentication does).
First real use case: registration. Hit /register and confirm a row lands in the identity table(s) with the expected fields (and that any Liberu-side profile/identity records are created in the same transaction). That single flow exercises the adapter, the model binding, and the event wiring — if it works, the rest usually does.
Smoke-test the four core actions before writing any product code: register, update profile, change password, reset password. These are the flows the bridge exists for; everything else is a variation.
Pattern: extend, don't fork. When identity rules diverge (extra fields, tenant assignment, invitation acceptance), subclass the bridge action and re-bind in your own provider. You keep upstream fixes and only own the delta.
namespace App\Auth;
use Liberu\Identity\Actions\CreateNewUser as BaseAction;
class CreateNewUser extends BaseAction
{
protected function afterCreate($user, array $input): void
{
parent::afterCreate($user, $input);
if ($invite = Invite::findByCode($input['invite_code'] ?? null)) {
$invite->accept($user);
}
}
}
Then rebind in FortifyServiceProvider (or your app provider) so the container resolves your class wherever the bridge bound the base one.
Pattern: keep Fortify config as the single switchboard. Because Fortify reads actions from config and caches it, treat config/fortify.php as the registration point and the bridge as the implementation. One place to look when "the wrong class ran."
'actions' => [
\App\Auth\CreateNewUser::class => \Laravel\Fortify\Contracts\CreatesNewUsers::class,
\Liberu\Identity\Actions\UpdateUserPassword::class => \Laravel\Fortify\Contracts\UpdatesUserPasswords::class,
],
Pattern: profile updates through the identity service, not $user->update(). The bridge's profile action should be your canonical write path — it handles any sync to identity/profile tables. If you're writing a controller or Livewire action that updates a user, route it through the same adapter so you don't get drift between users and identity records.
Pattern: test the adapter, not the UI. Adapter classes have narrow contracts, which makes them easy to test directly without Livewire/Inertia overhead:
public function test_bridge_creates_identity_record()
{
$user = app(CreatesNewUsers::class)->create([
'name' => 'A', 'email' => '[email protected]',
'password' => 'password', 'password_confirmation' => 'password',
]);
$this->assertDatabaseHas('identities', ['user_id' => $user->id]);
Event::assertDispatched(Registered::class);
}
Pattern: teams / multi-tenant flows. If you're on Jetstream teams, verify the bridge's create-user path runs before team attachment (Jetstream's AddTeamMember listener on the Registered event does the attaching). If the bridge creates the identity synchronously inside create(), order is fine; if it queues a job, you'll need a listener that waits or a ->afterCommit() arrangement.
Pattern: 2FA. Fortify's 2FA actions expect specific columns (two_factor_secret, two_factor_recovery_codes, two_factor_confirmed_at) and a HasTwoFactorAuthentication-style trait. If the bridge points Jetstream at a Liberu user model, check that trait and those columns exist on that model — otherwise enabling 2FA silently no-ops.
Integration tip: pair with laravel/ui-free flows. If you're API-first, the same adapters work behind Sanctum — you lose the Jetstream views but keep the action contracts, which is often the real reason to adopt a bridge like this.
config('fortify.actions') is read at boot. After changing action bindings, run php artisan config:clear (and optimize:clear in staging). Symptom: "my subclass isn't used" 90% of the time.Jetstream::useUserModel() and the bridge's own model config can disagree, and the resulting bug is confusing (relationships resolve on one class, actions hydrate another). Pick one, and assert it in a test: $this->assertSame(config('jetstream-bridge.models.user'), Jetstream::$userModel) after the provider boots.Registered, Login, Logout, PasswordReset. If the bridge's action dispatches a Liberu event instead of Laravel's Illuminate\Auth\Events\Registered, Jetstream's team-attachment and SendEmailVerificationNotification behavior quietly stops. Grep for event( / dispatch( in the adapter and confirm the canonical Laravel events still fire.Password::defaults() is configured in your FortifyServiceProvider. Bridge actions that validate independently can bypass it. Keep the rule definition in one place and reference it from both sides.users and an identity table in separate calls, a mid-flight exception (validation, mail failure, third-party identity API) leaves orphan rows. Prefer wrapping adapter calls in DB::transaction() at the call site if the package doesn't.app()->bound() / app()->make() in tinker. When an action "isn't doing what the docs say," resolve the contract from the container and dd(get_class(...)) — you'll immediately see whether the bridge or the default got bound, and whether a stray provider re-bound it later.composer.json and add a smoke test on your four core flows so a future release can't break auth silently.Fortify::authenticateUsing() for custom credential logic, (3) publish and override the bridge's config, (4) macro/event hooks. Reaching for (4) before (1) is usually a mistake — you'll be reimplementing what the adapter already does.jetstream:install and have app/Actions/Fortify/*, decide explicitly which set wins. Half-swapped actions (e.g. bridge's CreateNewUser + default's UpdateUserPassword) is the most common source of "password changes but profile doesn't sync" reports.How can I help you explore Laravel packages today?