orchid/fortify
Orchid Fortify integrates Laravel Fortify authentication into Orchid admin panels, providing login, registration, password reset, email verification, and two-factor authentication screens with configurable routes for secure Orchid applications.
orchid/fortify is a template repository, not a package you install. Do not composer require it — there is no tracked dependency to pin, and the value is entirely in the wiring you copy out of it. Clone it once into a scratch directory, read it, and port the pieces you need into your app:
git clone https://github.com/orchidsoftware/fortify /tmp/orchid-fortify
Assumed starting point: a working Orchid panel (orchid/platform installed, /admin reachable) and stock Fortify already installed:
composer require laravel/fortify
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
Where to look first, in this order:
FortifyServiceProvider — this is the integration in one file. It shows how Fortify::loginView(), registerView(), resetPasswordView(), twoFactorChallengeView(), and friends are rebound from stock Blade views to Orchid Screens. That mapping is the template's contribution; everything else is presentation.php artisan route:list on your install and note the collisions with /admin/*.LoginScreen first; the rest (RegisterScreen, ForgotPasswordScreen, ResetPasswordScreen, TwoFactorChallengeScreen, ConfirmPasswordScreen, VerifyEmailScreen) are variations on the same pattern.configureRateLimiting() and the Fortify action bindings — where login throttling and AttemptToAuthenticate / CreateNewUser / ResetUserPassword live.Your first use case, verbatim: get /admin/login rendering an Orchid-styled form that posts to Fortify's login route, and confirm a valid user lands inside the panel. Nothing else. That single flow validates the guard wiring, the view binding, and the layout assumption all at once. If it works, the remaining flows are mechanical; if it doesn't, you've spent an hour instead of a sprint.
Before you wire anything, grep the template's Screen base class against your pinned Orchid version. Orchid's Screen/Layout API has moved across majors and templates characteristically lag behind.
Bind Fortify views to Orchid Screens, and centralise every binding in one provider. Fortify resolves its entry points through Fortify::*View() callbacks; the template's trick is returning an Orchid Screen (or a minimal Blade shell that renders the Screen inside the panel layout) rather than a stock Blade view. Keep them all in one FortifyServiceProvider so the wiring is greppable:
// pattern: every Fortify view name resolves to an Orchid Screen
Fortify::loginView(fn () => (new LoginScreen())->build());
Fortify::resetPasswordView(fn ($request) => (new ResetPasswordScreen($request))->build());
One Screen per flow, small and flow-named. Build fields with Orchid's form builder (Input::make('email'), Password::make('password')) inside layouts() so styling and validation-error rendering come free through Orchid's TD/Layout machinery. Hand-written Blade inside a Screen is a smell — you've lost the reason you chose Orchid.
Keep Fortify's headless backend; replace only the surface. Customise through Fortify actions, not by editing the template's controllers:
// app/Providers/FortifyServiceProvider.php
Fortify::authenticateUsing(fn ($request) => /* custom lookup */);
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
The template shows the shape; your app-specific rules belong in app/Actions/Fortify/ where they're reviewable independent of the UI.
Align the guard explicitly. config/fortify.php → guard must match config/platform.php → guard (typically web, session driver). This is the single most common silent failure: valid credentials, no error, bounced back to the login screen.
Sequence the flows — do not port all of them at once. The dependency order that works:
password_reset_tokens migrated and a broker configured in config/auth.php)./email/verify).login.id), recovery codes, and the challenge redirect, and it's the flow most likely to need custom work.Enable features deliberately, and confirm a screen exists for each. In config/fortify.php:
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
Every enabled flag needs a reachable Screen in the template. A flag with no corresponding screen is a 500 the first time a real user hits that path — usually in production, because nobody exercises "the other" auth flows in staging.
Fold 2FA enrolment into your existing Orchid Profile screen rather than replacing it. Read the template's profile Screen diff, then port only the enable/disable/QR/recovery-code fields into a subclass of your profile Screen. You keep Orchid's profile behaviour and gain 2FA without forking an entire screen you'll now have to maintain across Orchid upgrades.
Throttling belongs in Fortify's provider, and is part of your auth review surface:
RateLimiter::for('login', fn (Request $request) => Limit::perMinute(5)
->by($request->input('email').'|'.$request->ip()));
Auditors ask about this. Treat the limiter definition as first-class code, not configuration.
Testing pattern: because view names are rebound to Screens, tests asserting view() names will break. Assert routes and Screen classes instead, and author feature tests for exactly the flows you would otherwise be trusting upstream on: throttled login, successful 2FA challenge, recovery-code consumption, password-reset token expiry. There is no CI in the upstream repo — that coverage is yours to write.
/login, /logout, /two-factor-challenge; Orchid owns /admin/*. Decide explicitly: either set fortify.prefix to admin (everything lives under the panel) or keep Fortify's routes and have the panel redirect to them. Running both unmodified yields redirect loops or a login 404, and the symptom is indistinguishable from a guard mismatch until you route:list.Fortify::loginView() pointing at a Blade view that doesn't exist — or at a Screen that throws inside build() — surfaces as a hard error only on /login, which is easy to miss until you log out in a hurry.verified middleware needs the verification routes wired too. If you gate the panel on verified, confirm the signed verification URL lands on an Orchid route. Otherwise users are permanently locked out with no UI path to complete verification.Screen/Layout APIs have broken across majors, and the failure mode is rendering, which often only appears in staging.php artisan vendor:publish --tag=orchid-assets, or orchid:install). Auth screens that look correct locally and broken in production are almost always an unpublished-asset problem.php artisan route:list | grep -E 'login|two-factor' → confirm config/fortify.php guard matches config/platform.php → dd() inside the Fortify::loginView callback to confirm which Screen is actually resolving → check session driver parity between local and staging.CreateNewUser, AttemptToAuthenticate, ResetUserPassword, UpdateUserProfileInformation, UpdateUserPassword) for behaviour; Fortify::redirects() for post-login destinations inside the panel; RateLimiter::for('login', ...) for throttling. Everything else in the template is presentation — edit it freely.How can I help you explore Laravel packages today?