Guardify π‘οΈ
A powerful, type-safe Role-Based UI Access Control (RBAC) and Permission-Based Access Control (PBAC) framework for Flutter apps, offering both Annotation-Based Code Generation and Zero-Boilerplate Runtime Widgets.
π Security Best Practices & Architectural Notice
Important
Client-Side UI Gating vs. Server-Side Enforcement (Defense in Depth)
Guardify is designed for the Presentation Layer (UI/UX):
- It prevents unauthorized UI components from rendering or executing client-side callbacks (
onPressed,onTap). - It delivers smooth, clean UX by hiding restricted buttons, displaying disabled states with lock badges, and handling route redirects before screens mount.
However, client-side code runs on user-controlled hardware and can theoretically be inspected or manipulated:
- Never rely exclusively on client-side UI gating for data security.
- Server-Side Enforcement is Mandatory: Sensitive data operations, database queries, and mutation endpoints (e.g. DELETE / POST API calls) must always be authenticated and authorized on the backend server or cloud security layer (e.g., Firebase Security Rules, Supabase Row-Level Security (RLS), Node.js / Go / Spring Boot JWT claim validators).
- Verified Claims: Always supply
currentRole,currentRoles, orcurrentPermissionstoGuardifyScopefrom cryptographically verified authentication sources (such as verified JWT token claims, Firebase Auth Custom Claims, or Auth0 access tokens). - Secure by Default: Guardify follows the principle of least privilegeβif roles or permissions do not match, or if
GuardifyScopeis missing from the tree, access is safely denied by default.
β¨ Features
- π‘οΈ Dual-Mode Access Control:
- Runtime Widget (
RoleGate/SecuredFeature): Protect any Flutter or 3rd-party widget on-the-fly with Zerobuild_runnerrequired. - Code Generation (
@Secured): Type-safe, component-level security policies declared on widget classes.
- Runtime Widget (
- β‘ Zero Prop-Drilling: Ambient role & permission state resolution via
GuardifyScope(InheritedWidget). - π Reactive State Binding: Connect directly to
ValueListenable,ChangeNotifier, orStreamusingGuardifyScope.listenableorGuardifyScope.streamfor automatic rebuilds on auth changes. - π¨ Flexible Fallback UI Strategies:
FallbackType.hide(default): Silently hides unauthorized widgets (const SizedBox.shrink()).FallbackType.disabled: Disables user interactions (AbsorbPointer) with reduced opacity and optional lock overlay icon.FallbackType.scaffold: Renders a full "Access Denied" page for screens.FallbackType.text: Displays an inline error text widget.- Dynamic Fallback Builders: Pass
fallbackBuilder: (context, missingRoles, missingPerms) => ...locally on widgets or globally inGuardifyScope.
- π₯ Multi-Role & PBAC Support: Configure roles and fine-grained permissions (
requireAll: true/false). - π§ Route Guard for Navigation: Easily secure routes with
GuardifyRouteGuardorguardifyRedirect(compatible withgo_routerand Navigator). - π Custom Permission Checker: Plug in custom auth callbacks (Firebase Auth, JWT claims, dynamic rules).
- π BuildContext Extensions: Query authorization directly in code using
context.hasRole('admin')orcontext.hasPermission('users:delete'). - π‘οΈ Variable Shadowing Immunity: Code generator safely isolates internal variables to avoid any parameter collisions.
π Getting Started
Add Dependencies
Add guardify to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
guardify: ^1.2.0
dev_dependencies:
build_runner: ^2.4.0 # Optional: only needed if using @Secured code generation
Run flutter pub get to install.
π» Usage
1. Wrap Your App with GuardifyScope
Wrap your app or widget tree with GuardifyScope to supply active user roles and permissions:
void main() {
runApp(
GuardifyScope(
currentRole: 'admin',
currentPermissions: const {'users:delete', 'reports:export'},
child: const MyApp(),
),
);
}
Reactive Auth Binding with GuardifyScope.listenable
Automatically rebuilds descendant scope widgets whenever your auth notifier changes (no setState required):
GuardifyScope.listenable(
listenable: authStateNotifier, // ValueNotifier<String?>
child: const MyApp(),
)
2. Choose Your Gating Mode
Option A: Zero-Boilerplate Runtime Widget (RoleGate)
No code generation required! Guard any widget directly:
// 1. Silently hide for unauthorized users
RoleGate(
allowedRoles: const ['admin'],
child: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
)
// 2. Disabled mode with lock overlay badge (e.g. Freemium / Pro features)
RoleGate(
allowedRoles: const ['premium'],
fallbackType: FallbackType.disabled,
lockBadge: const CircleAvatar(
backgroundColor: Colors.black87,
child: Icon(Icons.lock, color: Colors.amber),
),
child: ElevatedButton(onPressed: exportReport, child: const Text('Export PDF')),
)
// 3. Dynamic context-aware fallback builder
RoleGate(
allowedRoles: const ['editor'],
fallbackBuilder: (context, missingRoles, missingPerms) {
return UpgradeBanner(requiredRole: missingRoles.first);
},
child: const EditArticleToolbar(),
)
Option B: Annotation-Based Code Generation (@Secured)
Ideal for design systems, reusable button kits, and enterprise component governance:
part 'my_widgets.secured.g.dart';
@Secured(['admin', 'superadmin'], fallback: FallbackType.hide)
class DeleteUserButton extends StatelessWidget {
final String userId;
final VoidCallback onDelete;
const DeleteUserButton({super.key, required this.userId, required this.onDelete});
@override
Widget build(BuildContext context) => ElevatedButton(...);
}
Run build runner to generate .secured.g.dart:
dart run build_runner build --delete-conflicting-outputs
Then instantiate cleanly with zero prop-drilling:
SecuredDeleteUserButton(
userId: 'usr_100',
onDelete: () => deleteUser('usr_100'),
);
3. Route & Navigation Guards (go_router / Navigator)
Prevent unauthorized users from entering protected screens at the router level:
GoRoute(
path: '/admin',
redirect: GuardifyRouteGuard(
allowedRoles: const ['admin'],
fallbackPath: '/unauthorized',
),
builder: (context, state) => const AdminScreen(),
);
Or perform an inline redirect check:
final redirect = guardifyRedirect(
context,
allowedRoles: const ['manager'],
fallbackPath: '/unauthorized',
);
if (redirect != null) {
// Handle redirect or show notification
}
4. BuildContext Extensions
Use BuildContext helper extensions anywhere in your widget tree:
// Role checks
if (context.hasRole('admin')) { ... }
if (context.hasAnyRole(['admin', 'manager'])) { ... }
if (context.hasAllRoles(['manager', 'finance'])) { ... }
// Permission checks (PBAC)
if (context.hasPermission('users:delete')) { ... }
if (context.hasAnyPermission(['reports:view', 'reports:export'])) { ... }
if (context.hasAllPermissions(['posts:read', 'posts:write'])) { ... }
5. Fallback UI Strategies
| Strategy | Behavior | Typical Use Case |
|---|---|---|
FallbackType.hide (default) |
Renders const SizedBox.shrink() |
Inline buttons, list actions |
FallbackType.disabled |
Renders with AbsorbPointer, reduced opacity, & optional lock badge |
Freemium / Pro features |
FallbackType.scaffold |
Full-screen "Access Denied" page with AppBar | Restricted screens / pages |
FallbackType.text |
Renders inline error text message | Form fields, tables |
fallbackBuilder |
Dynamic context-aware builder receiving missing roles & permissions | Custom upgrade banners |
π‘οΈ Custom Permission Checker Callback
Connect external authentication engines or custom claim evaluators:
GuardifyScope(
permissionChecker: (allowedRoles, {requireAll = false, activeRoles, allowedPermissions, activePermissions}) {
return myAuthService.evaluate(
allowedRoles: allowedRoles,
allowedPermissions: allowedPermissions,
);
},
child: const MyApp(),
)
πΊοΈ Roadmap
Check out our detailed ROADMAP.md to explore upcoming features including compliance CLI tools and automated test scopes.
π License
This project is licensed under the MIT License - see the LICENSE file for details.