winter-bridge/bridge
Bridge connects Winter CMS with your Laravel/PHP application, offering a lightweight integration layer for shared configuration, components, and services. Install via Composer and wire it into your existing stack with minimal changes.
Install it like any Laravel package and verify version compatibility before anything else. The newest tag is 1.0.1, which aligned its AuthManager with current WinterCMS — but the release note says nothing about Laravel or illuminate/* constraints, so Winter-side compatibility being refreshed does not automatically mean your Laravel version is supported.
composer require winter-bridge/bridge
The package is auto-discovered via its extra.laravel.providers entry. Confirm that by running:
php artisan package:discover
php artisan about
Where to look first, in this order:
composer.json of the package — check the illuminate/* and Winter core (wintercms/winter or winter/wn-*) constraints. Read the Winter constraint against 1.0.1 (it should now target a current Winter branch) and, separately, confirm the illuminate/* constraint against your Laravel version. These two questions have different answers.src/ — look for the service provider, the AuthManager (the class touched by 1.0.1), a Guard class, a UserProvider class, and any config file under config/. That is the entire surface area of a bridge package.config/auth.php in your app — this is where the package actually takes effect, not in the package itself.First use case. The bridge exists so that Laravel's Auth facade can authenticate against Winter's user records instead of your app's own users table. The minimal viable setup is to point a guard at the package's provider:
// config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'winter', // name may differ — check the provider registration in the AuthManager
],
],
'providers' => [
'winter' => [
'driver' => 'winter', // or an Eloquent provider pointed at Winter's model
'model' => \Winter\Storm\Auth\Models\User::class, // verify the actual class in src/
],
],
Then prove it end-to-end with a throwaway route before building login UI:
Route::get('/whoami', fn () => Auth::user()?->email);
If that returns a Winter user after a manual Auth::login($user), the bridge is wired correctly and the rest is ordinary Laravel auth work.
If you are upgrading from a pre-1.0.1 install, treat it as a semi-breaking change even though the version is a patch: the AuthManager is precisely the object that registers guard drivers and resolves providers. Re-run the "whoami" check and the debugging checklist below before rolling to production.
Wrap the bridge in your own guard name. Don't overload web. Register something like backend or winter and leave web on your native App\Models\User. Multi-guard setups are the normal case here — you will almost always have both a Laravel-side user and a Winter-side user in the same application.
'guards' => [
'web' => ['driver' => 'session', 'provider' => 'users'],
'winter' => ['driver' => 'session', 'provider' => 'winter'],
],
Protect route groups with an explicit guard. Laravel's auth middleware defaults to the default guard, which is a common source of "logged in but still redirected" bugs:
Route::middleware('auth:winter')->group(function () {
Route::get('/admin', AdminController::class);
});
// and in your login controller
Auth::guard('winter')->attempt($credentials);
Do not use Auth::attempt() on the default guard by accident. A LoginController copied from Breeze/Fortify calls Auth::attempt(), which resolves to auth.defaults.guard. Either set the guard explicitly in every call, or override guard() in your controller.
Log in the model, not the credentials, when bridging records. If you already have a Winter user (e.g. resolved from a plugin, an SSO callback, or an existing session), Auth::guard('winter')->login($winterUser) is more reliable than attempt(), because it sidesteps any mismatch between the package's credential validation and Winter's password hashing.
Reuse Winter's authorization instead of rebuilding it. If Winter's backend user model is exposed through the bridge, permission checks belong on that model, not on a duplicate Laravel policy layer:
if (! Auth::guard('winter')->user()?->hasPermission('winter.blog.manage_posts')) {
abort(403);
}
Wrap that in a small middleware so it composes with Laravel's can/Gate if you need it.
Fire Laravel's auth events. The bridge should let the standard Illuminate\Auth\Events\Login, Logout, and Failed events through. Listen to them for auditing and session bookkeeping rather than hooking into Winter's own events — this keeps your app portable if you later replace the bridge.
Re-verify the manager after any patch bump. Because 1.0.1 modified the AuthManager, pin the version bump to a tested commit and diff src/ before and after. Driver names and provider resolution order live in that class; a rename there breaks config/auth.php silently (you get "provider not defined", not a deprecation notice).
Test with an explicit guard. $this->actingAs($user, 'winter') works once the guard is registered. Any custom guard driver that isn't session will need a real HTTP-level test against your login route; don't assume actingAs covers provider logic.
Integration tip for Fortify/Breeze/Jetstream: these packages are opinionated about guard names. The least painful path is to configure their guard option (Fortify has fortify.guard) to your bridge guard, rather than patching the bridge to impersonate the web guard.
Maintenance risk is reduced but not gone. 1.0.1 is an active response to upstream WinterCMS changes, which is a good sign the author is still watching the ecosystem — but the package remains low-adoption (single-digit stars, no dependents) and the release note ("Do tell me if you encounter any issues") reads as lightly tested. Pin explicitly in composer.json and read the AuthManager diff. Keep the fork-and-vcs-repository plan in your back pocket; it is cheap to set up and expensive to need urgently.
Separate the two compatibility questions. 1.0.1 addressed Winter drift, not necessarily Laravel drift. Check the illuminate/* constraint in composer.json directly — if it still caps at Laravel 8/9, installation may succeed on a newer Laravel while the Guard and UserProvider contracts have moved on (notably around Illuminate\Contracts\Auth\Guard signatures and session-guard rehash behavior). If Auth::user() returns null, check the provider's retrieveById implementation against the current contract first.
Verify what "user" means before you wire anything. Winter has backend (admin) users and frontend users supplied by plugins like RainLab.User. The repository description says "Winter Core Extending Laravel Auth," which points at backend/core users — confirm this in the source. If you need frontend users, the bridge may not be what you want at all.
Password hashing must match. If attempt() silently fails while the user exists, compare your app's config/hashing.php driver and Hash::check() against a known-good Winter password. Winter has historically used bcrypt; an Argon2id or differently-configured Laravel app will produce false negatives with no useful error.
Session and cookie collisions. Two session guards sharing one cookie can log each other out, especially if both use the login_web_* session key. Give the bridge guard its own session key where possible, and consider separate session drivers for admin and public areas.
remember_token and schema drift. Remember-me and Auth::viaRemember() depend on column names matching what the provider expects (remember_token in Laravel, different in Winter's schema). Test the "remember me" checkbox explicitly rather than trusting it.
Config cache will hide config edits. After touching anything in config/auth.php or the package config, run php artisan config:clear. This wastes an embarrassing amount of debugging time when a guard change appears not to apply — and it is the first thing to suspect after upgrading to 1.0.1, when a cached auth config can mask the new manager's behaviour.
Debugging checklist, in order:
dump(config('auth.guards.winter')); // is the guard registered?
dump(Auth::guard('winter')->check()); // does the guard resolve?
dump(Auth::guard('winter')->user()?->getAuthIdentifier());
dump(Auth::guard('winter')->getProvider()); // right provider class?
If getProvider() throws InvalidArgumentException: Authentication user provider [winter] is not defined, the package's service provider or its AuthManager didn't register the driver — check that discovery actually ran, that the provider is not deferred behind a condition, and (post-upgrade) that the driver key name didn't change in 1.0.1.
Extension points to prefer. Rather than editing vendor code: bind your own UserProvider implementation in a service provider (Laravel resolves providers from config/auth.php by class name), decorate the package's guard, and use Event::listen on the standard auth events. Since 1.0.1 proves the AuthManager is a moving target, keeping the vendor directory untouched means composer update stays a version bump rather than a merge conflict.
Security posture. If the bridge authenticates admin-level Winter users, treat those routes as privileged: separate middleware stack, rate limiting on the login route, and no serialization of the full Winter user model into JSON responses. Also confirm the bridge doesn't accidentally leave Winter's administrative permissions accessible to ordinary web-guard users through a shared model.
How can I help you explore Laravel packages today?