Guides

Route Protection

Guard routes and screens with GoRouter's redirect, or with authyra_flutter's Navigator-based guard widgets.

Authyra doesn't force a routing pattern. Pick the one that matches your app.


No special widget needed, wire authStateChanges straight into refreshListenable and redirect:

lib/router.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:go_router/go_router.dart';

class _StreamListenable extends ChangeNotifier {
  _StreamListenable(Stream<dynamic> stream) {
    stream.listen((_) => notifyListeners());
  }
}

final router = GoRouter(
  refreshListenable: _StreamListenable(Authyra.instance.authStateChanges),
  redirect: (context, state) {
    final authenticated = Authyra.instance.isAuthenticated; // synchronous
    final goingToLogin = state.uri.path == '/login';

    if (!authenticated && !goingToLogin) return '/login';
    if (authenticated && goingToLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
    GoRoute(path: '/', builder: (_, __) => const HomePage()),
  ],
);

isAuthenticated is the synchronous cache, safe to read inside redirect without await.


Widget-level guards

For protecting a piece of UI rather than an entire route (a "delete account" button, an admin panel section):

RequireAuth

RequireAuth(
  fallback: const Text('Sign in to continue'),
  child: ElevatedButton(
    onPressed: deleteAccount,
    child: const Text('Delete account'),
  ),
)

Shows child only while a non-expired session exists; fallback otherwise. Reacts live to sessionStream.

RequireRole

RequireRole(
  role: 'admin',
  fallback: const Text('Admins only'),
  child: const AdminPanel(),
)

Checks session.user.metadata['role'] against role. Your AuthUser.metadata has to actually carry a 'role' entry, populate it from your provider's signIn result.


If your app uses plain Navigator (not GoRouter, AutoRoute, or another declarative router), authyra_flutter ships guard widgets for that case specifically:

AuthyraGuard(
  config: AuthyraGuardConfig(
    loginRoute: '/login',
    // or: loginBuilder: (context) => const LoginPage(),
  ),
  child: const HomePage(),
)
AuthyraInverseGuard(
  config: AuthyraGuardConfig(homeRoute: '/home'),
  child: const LoginPage(), // redirects away if already authenticated
)

Or guard at the navigator level with AuthyraNavigationObserver:

MaterialApp(
  navigatorObservers: [
    AuthyraNavigationObserver(
      loginRoute: '/login',
      protectedRoutes: ['/profile', '/settings', '/admin'],
    ),
  ],
)

Or wrap the whole app's top-level split with AuthyraRouter:

MaterialApp(
  home: AuthyraRouter(
    authenticatedBuilder: (context) => const HomePage(),
    unauthenticatedBuilder: (context) => const LoginPage(),
    loadingBuilder: (context) => const SplashScreen(),
  ),
)
AuthyraGuard, AuthyraInverseGuard, AuthyraNavigationObserver, and AuthyraRouter are deliberately not for GoRouter/AutoRoute apps, those routers have their own redirect mechanism, shown above, and mixing the two causes fights over navigation. Reach for these only when you're on plain Navigator.

See also

Copyright © 2026