access_gate 0.0.3
access_gate: ^0.0.3 copied to clipboard
Hide Flutter widgets behind feature flags, roles, permissions, and attribute-based access policies.
access_gate #
Hide Flutter widgets behind feature flags, roles, permissions, and attribute-based access policies.
access_gate is intentionally provider-agnostic. Bring your own source of
truth, such as Firebase Remote Config, LaunchDarkly, Supabase claims, local app
state, or a backend policy response, then expose those facts through
AccessContext.
Features #
- Feature flag gates with enabled flags and exact feature values.
- RBAC gates with required roles or any-of role checks.
- Permission gates for fine-grained access checks.
- ABAC gates with exact attributes and custom predicates.
- Policy composition with all-of, any-of, and not rules.
- Structured denial reasons for custom fallback UI.
AccessGuardfor page-level access decisions.- JSON helpers for access contexts and serializable policies.
AccessScopeandAccessControllerfor inherited access state.AccessHidden, a zero-size fallback for denied gates.
Getting started #
Add the package, then create an AccessController from your app's current
feature flag and authorization state.
final accessController = AccessController(
AccessContext(
enabledFeatures: {'advanced_reports'},
roles: {'admin'},
permissions: {'reports.view'},
attributes: {'plan': 'pro', 'region': 'us'},
),
);
Place AccessScope above the widgets that should use the shared access state.
AccessScope(
controller: accessController,
child: const MyApp(),
);
Basic usage #
Require one feature flag:
AccessGate.feature(
feature: 'advanced_reports',
child: const AdvancedReportsButton(),
);
Require a role:
AccessGate.role(
role: 'admin',
fallback: const Text('Admin access required'),
child: const AdminPanel(),
);
Require a permission:
AccessGate.permission(
permission: 'reports.view',
child: const ReportList(),
);
Combine feature flags, roles, permissions, and ABAC attributes:
AccessGate.when(
allFeatures: {'advanced_reports'},
anyRoles: {'admin', 'analyst'},
allPermissions: {'reports.view'},
attributes: {'plan': 'pro'},
child: const AdvancedReportsPage(),
);
Use a custom predicate when exact attribute matching is not enough:
AccessGate.when(
predicate: (context) {
return context.attribute('teamId') == selectedTeamId &&
context.attribute('plan') == 'enterprise';
},
predicateReason: 'Requires access to this team.',
child: const TeamSettings(),
);
Policy composition #
Compose policies when access can be granted through multiple paths, or when an allow rule needs an explicit exclusion.
final policy = AccessPolicy.allOf([
AccessPolicy.anyOf([
AccessPolicy.role('admin'),
AccessPolicy(
allPermissions: {'reports.view'},
attributes: {'plan': 'pro'},
),
]),
AccessPolicy.not(
AccessPolicy.role('suspended'),
reason: 'Suspended users cannot access reports.',
),
]);
AccessGate(
policy: policy,
child: const AdvancedReportsPage(),
);
Page guards #
Use AccessGuard when the whole page or route body should branch on an access
decision.
AccessGuard(
policy: AccessPolicy.permission('reports.view'),
builder: (context, decision) {
return const ReportsPage();
},
deniedBuilder: (context, decision) {
return Text(decision.reasons.first);
},
);
Typed keys #
The core API stores provider-facing strings, but apps can define typed keys with enums by implementing the category marker interfaces.
enum AppFeature implements AccessFeature {
advancedReports('advanced_reports');
const AppFeature(this.accessKey);
@override
final String accessKey;
}
enum AppRole implements AccessRole {
admin('admin');
const AppRole(this.accessKey);
@override
final String accessKey;
}
Use fromKeys and *Key constructors when you want compile-time key names in
app code:
final context = AccessContext.fromKeys(
enabledFeatures: {AppFeature.advancedReports},
roles: {AppRole.admin},
);
AccessGate.featureKey(
feature: AppFeature.advancedReports,
child: const AdvancedReportsButton(),
);
If an enum's case name already matches the provider key, the AccessEnumKey
extension exposes myEnumValue.accessKey as a convenience.
Denied access #
By default, denied gates render accessHidden, a zero-size widget that is safe
inside layout widgets like Column, Row, and Stack. This is useful when a
widget should simply disappear.
AccessGate.feature(
feature: 'new_checkout',
child: const CheckoutButton(),
);
Use fallback or fallbackBuilder when denied users should see something.
AccessGate.permission(
permission: 'billing.manage',
fallbackBuilder: (context, decision) {
final reason = decision.denialReasons.first;
return Text('${reason.key}: ${reason.message}');
},
child: const BillingSettings(),
);
decision.reasons remains available as a simple list of messages.
JSON helpers #
AccessContext can round-trip through JSON-compatible maps. Policies without
custom predicates can also be serialized, including composed policies.
final context = AccessContext.fromJson(savedContextJson);
final policy = AccessPolicy.fromJson(savedPolicyJson);
final contextJson = context.toJson();
final policyJson = policy.toJson();
Custom predicate functions are runtime-only and cannot be serialized.
Agent-friendly usage #
Coding agents integrating this package into Flutter apps should read
doc/using-access-gate.md.
The shared Agent Skills-compatible skill lives at skills/access-gate/. Codex
can use it from that location. Claude Code users can copy that folder to
~/.claude/skills/access-gate/, or use the project wrapper at
.claude/skills/access-gate/ when working from this repository.
Repository-maintenance guidance is available in AGENTS.md for Codex and
CLAUDE.md for Claude Code.
Important security note #
access_gate controls client-side visibility. It is not a replacement for
server-side authorization, database security rules, API checks, or audit
controls. Use it to keep Flutter UI honest and ergonomic; enforce real access
at your data and service boundaries too.