Role-Based Access Control System in Mobile App
Mobile app permissions — not just if (user.role == "admin") before button. When roles multiply, nested permissions appear, temporary access, granular policies per resource — primitives become scattered chaos, untestable and easy to break.
Access Control Models
Choose model first. Three main options:
RBAC (Role-Based Access Control) — role attached to permission set. User gets role, role determines access. Simple, clear, works for most B2B apps. Problem: roles proliferate. Starts with admin, manager, viewer — ends with 40 overlapping roles.
ABAC (Attribute-Based Access Control) — access determined by subject, resource, context attributes. Rule like allow if user.department == resource.department AND action == "read" AND time.hour < 18. More flexible than RBAC, harder to implement.
ReBAC (Relationship-Based Access Control) — access via relationship graph. Google Zanzibar, open-source: OpenFGA, SpiceDB. Good for systems like Google Drive.
For most corporate mobile apps, RBAC with permission flags sufficient.
Client Implementation
Golden rule: client trusts nothing about itself. Permission checks on mobile — UX layer, not protection. Real protection — backend. Bad UX without proper permissions makes buttons user can't click or hides available features.
On Android via ViewModel:
data class UserPermissions(
val canCreateOrder: Boolean,
val canApproveOrder: Boolean,
val canViewReports: Boolean,
val managedDepartments: List<String>
)
class OrderViewModel(
private val permissionsRepository: PermissionsRepository
) : ViewModel() {
val permissions = permissionsRepository.currentPermissions
.stateIn(viewModelScope, SharingStarted.Eagerly, UserPermissions())
fun createOrder(order: Order) {
check(permissions.value.canCreateOrder) { "Access denied" }
// ...
}
}
In Compose hide unavailable elements, not just disable:
val permissions by viewModel.permissions.collectAsState()
if (permissions.canCreateOrder) {
Button(onClick = { viewModel.createOrder(draft) }) {
Text("Create Request")
}
}
On iOS similar via @Published in ObservableObject:
class PermissionsManager: ObservableObject {
@Published private(set) var permissions: UserPermissions = .empty
func refreshPermissions() async {
guard let token = authService.currentToken else { return }
permissions = try await permissionsAPI.fetch(token: token)
}
}
Storage and Synchronization
Permissions come from backend on login and cache locally. Critical: cache invalidates on role change. Standard approach — TTL (15–30 minutes) + forced refresh on session start.
If role changes real-time (manager revokes access), need WebSocket notifications or server-sent events with forced permission refresh without re-login.
On Android — EncryptedSharedPreferences for permission cache. On iOS — Keychain for token, UserDefaults with Codable encoding for permissions cache.
Common Issues
Race condition on login. Permissions load parallel with navigation. User clicks before permissions arrive. Solution: blocking load screen until permissions fetched, or optimistic UI with recheck.
Hardcoded checks scattered. if (role == "admin") in 30 screens. Refactoring painful. Solution from start: centralized PermissionsManager, UI reads only from it.
No logging on denial. Backend rejects with 403, mobile shows generic error. Unknown who, what, why couldn't do. Need logging via Firebase Analytics or Sentry with permission_denied event and action context.
Timeline and Cost
Design access model, implement PermissionsManager, API integration, UI adaptation, testing — 2–4 weeks depending on role count and screens. If ReBAC with OpenFGA needed — add backend setup. Cost estimated individually.







