Skip to Content
AuthenticationPermissions

InstantDB provides flexible permission systems through user roles, entity-level access control, and custom permission logic. Build secure applications with fine-grained access control that scales with your needs.

Understanding Permissions

Permission Concepts

InstantDB permissions are based on several key concepts:

  • User Roles: Define what a user can do (admin, editor, viewer)
  • Entity Access: Control access to specific data entities
  • Operation Permissions: Control create, read, update, delete operations
  • Attribute-level Access: Control access to specific fields

User Roles

Basic Role System

Implement a simple role-based system:

enum UserRole { admin, editor, viewer, guest, } extension UserRoleExtension on UserRole { String get displayName { switch (this) { case UserRole.admin: return 'Administrator'; case UserRole.editor: return 'Editor'; case UserRole.viewer: return 'Viewer'; case UserRole.guest: return 'Guest'; } } bool get canWrite { return this == UserRole.admin || this == UserRole.editor; } bool get canDelete { return this == UserRole.admin; } bool get canManageUsers { return this == UserRole.admin; } } class UserPermissions { final AuthUser user; late final UserRole role; UserPermissions(this.user) { // Extract role from user metadata final roleString = user.type as String?; role = UserRole.values.firstWhere( (r) => r.name == roleString, orElse: () => UserRole.guest, ); } bool canRead(String entityType) { // All authenticated users can read return true; } bool canCreate(String entityType) { switch (entityType) { case 'posts': case 'comments': return role.canWrite; case 'users': return role.canManageUsers; default: return role.canWrite; } } bool canUpdate(String entityType, Map<String, dynamic> entity) { // Users can update their own content if (entity['authorId'] == user.id) return true; // Admins can update anything if (role == UserRole.admin) return true; // Editors can update posts and comments if (role == UserRole.editor && ['posts', 'comments'].contains(entityType)) { return true; } return false; } bool canDelete(String entityType, Map<String, dynamic> entity) { // Only admins can delete, or users can delete their own content return role == UserRole.admin || entity['authorId'] == user.id; } }

Role Management UI

Create interfaces for managing user roles:

class RoleManagementScreen extends StatefulWidget { @override State<RoleManagementScreen> createState() => _RoleManagementScreenState(); } class _RoleManagementScreenState extends State<RoleManagementScreen> { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value!; final userPermissions = UserPermissions(currentUser); if (!userPermissions.canManageUsers) { return Scaffold( appBar: AppBar(title: const Text('Access Denied')), body: const Center( child: Text('You do not have permission to manage users.'), ), ); } return Scaffold( appBar: AppBar( title: const Text('Role Management'), ), body: InstantBuilder( query: { 'users': { 'orderBy': {'createdAt': 'desc'}, } }, builder: (context, result) { final users = (result.data?['users'] as List? ?? []) .cast<Map<String, dynamic>>(); return ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return UserRoleCard( user: user, onRoleChanged: (newRole) => _updateUserRole(user['id'], newRole), ); }, ); }, ), ); } Future<void> _updateUserRole(String userId, UserRole newRole) async { final db = InstantProvider.of(context); await db.transact([ db.update(userId, { 'role': newRole.name, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('User role updated to ${newRole.displayName}'), ), ); } } class UserRoleCard extends StatelessWidget { final Map<String, dynamic> user; final Function(UserRole) onRoleChanged; const UserRoleCard({ super.key, required this.user, required this.onRoleChanged, }); @override Widget build(BuildContext context) { final currentRole = UserRole.values.firstWhere( (r) => r.name == (user['role'] as String? ?? 'guest'), orElse: () => UserRole.guest, ); return Card( margin: const EdgeInsets.all(8), child: ListTile( leading: CircleAvatar( child: Text(user['email']?.toString().substring(0, 1).toUpperCase() ?? '?'), ), title: Text(user['email']?.toString() ?? 'Unknown'), subtitle: Text('Role: ${currentRole.displayName}'), trailing: DropdownButton<UserRole>( value: currentRole, onChanged: (newRole) { if (newRole != null) { onRoleChanged(newRole); } }, items: UserRole.values.map((role) { return DropdownMenuItem( value: role, child: Text(role.displayName), ); }).toList(), ), ), ); } }

Entity-Level Permissions

Ownership-Based Access

Implement ownership-based permissions:

class OwnershipPermissions { final AuthUser currentUser; OwnershipPermissions(this.currentUser); bool canAccess(Map<String, dynamic> entity, String operation) { switch (operation) { case 'read': return _canRead(entity); case 'update': return _canUpdate(entity); case 'delete': return _canDelete(entity); default: return false; } } bool _canRead(Map<String, dynamic> entity) { // Public entities can be read by anyone if (entity['isPublic'] == true) return true; // Private entities can only be read by owner if (entity['ownerId'] == currentUser.id) return true; // Shared entities can be read by collaborators final collaborators = entity['collaborators'] as List?; if (collaborators?.contains(currentUser.id) == true) return true; return false; } bool _canUpdate(Map<String, dynamic> entity) { // Only owner or collaborators with edit permission if (entity['ownerId'] == currentUser.id) return true; final permissions = entity['permissions'] as Map<String, dynamic>?; final userPermission = permissions?[currentUser.id] as String?; return userPermission == 'edit' || userPermission == 'admin'; } bool _canDelete(Map<String, dynamic> entity) { // Only owner can delete return entity['ownerId'] == currentUser.id; } } // Usage in widgets class SecurePostList extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value!; final permissions = OwnershipPermissions(currentUser); return InstantBuilder( query: {'posts': {}}, builder: (context, result) { final posts = (result.data?['posts'] as List? ?? []) .cast<Map<String, dynamic>>() .where((post) => permissions.canAccess(post, 'read')) .toList(); return ListView.builder( itemCount: posts.length, itemBuilder: (context, index) { final post = posts[index]; return PostCard( post: post, canEdit: permissions.canAccess(post, 'update'), canDelete: permissions.canAccess(post, 'delete'), ); }, ); }, ); } }

Team-Based Permissions

Implement team-based access control:

class TeamPermissions { final AuthUser currentUser; final Map<String, dynamic> team; TeamPermissions({required this.currentUser, required this.team}); UserRole get userRoleInTeam { final members = team['members'] as Map<String, dynamic>? ?? {}; final roleString = members[currentUser.id] as String?; return UserRole.values.firstWhere( (r) => r.name == roleString, orElse: () => UserRole.guest, ); } bool get isMember => userRoleInTeam != UserRole.guest; bool get isAdmin => userRoleInTeam == UserRole.admin; bool get canEdit => userRoleInTeam.canWrite; bool canAccessProject(Map<String, dynamic> project) { // Check if project belongs to team if (project['teamId'] != team['id']) return false; // Check if user has access to team return isMember; } bool canManageTeam() { return isAdmin; } bool canInviteMembers() { return userRoleInTeam == UserRole.admin || userRoleInTeam == UserRole.editor; } } // Team access control widget class TeamAccessControl extends StatelessWidget { final Map<String, dynamic> team; final Widget child; const TeamAccessControl({ super.key, required this.team, required this.child, }); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; if (currentUser == null) { return const Center(child: Text('Please sign in')); } final teamPermissions = TeamPermissions( currentUser: currentUser, team: team, ); if (!teamPermissions.isMember) { return Scaffold( appBar: AppBar(title: const Text('Access Denied')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.lock, size: 64, color: Colors.grey), const SizedBox(height: 16), Text( 'You don\'t have access to this team', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 24), ElevatedButton( onPressed: () => _requestAccess(context), child: const Text('Request Access'), ), ], ), ), ); } return child; } void _requestAccess(BuildContext context) { // Implementation for requesting team access ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Access request sent to team admins')), ); } }

Field-Level Permissions

Attribute Access Control

Control access to specific fields:

class AttributePermissions { final AuthUser currentUser; final UserRole role; AttributePermissions(this.currentUser, this.role); Map<String, dynamic> filterReadableFields( String entityType, Map<String, dynamic> entity, ) { final filtered = <String, dynamic>{}; entity.forEach((key, value) { if (canReadField(entityType, key, entity)) { filtered[key] = value; } }); return filtered; } bool canReadField( String entityType, String fieldName, Map<String, dynamic> entity, ) { switch (entityType) { case 'users': return _canReadUserField(fieldName, entity); case 'posts': return _canReadPostField(fieldName, entity); default: return true; // Default allow read } } bool canWriteField( String entityType, String fieldName, Map<String, dynamic> entity, ) { switch (entityType) { case 'users': return _canWriteUserField(fieldName, entity); case 'posts': return _canWritePostField(fieldName, entity); default: return role.canWrite; } } bool _canReadUserField(String fieldName, Map<String, dynamic> entity) { // Public fields anyone can read const publicFields = {'id', 'name', 'avatar', 'createdAt'}; if (publicFields.contains(fieldName)) return true; // Private fields only owner or admin can read const privateFields = {'email', 'phone', 'address'}; if (privateFields.contains(fieldName)) { return entity['id'] == currentUser.id || role == UserRole.admin; } return true; } bool _canWriteUserField(String fieldName, Map<String, dynamic> entity) { // Users can edit their own profile if (entity['id'] == currentUser.id) { const editableFields = {'name', 'avatar', 'bio'}; return editableFields.contains(fieldName); } // Admins can edit role and status if (role == UserRole.admin) { const adminFields = {'role', 'status', 'permissions'}; return adminFields.contains(fieldName); } return false; } bool _canReadPostField(String fieldName, Map<String, dynamic> entity) { // Draft posts only visible to author if (entity['status'] == 'draft') { return entity['authorId'] == currentUser.id || role == UserRole.admin; } return true; } bool _canWritePostField(String fieldName, Map<String, dynamic> entity) { // Author can edit content fields if (entity['authorId'] == currentUser.id) { const contentFields = {'title', 'content', 'tags'}; return contentFields.contains(fieldName); } // Admins can edit metadata if (role == UserRole.admin) { const metaFields = {'status', 'featured', 'priority'}; return metaFields.contains(fieldName); } return false; } }

Secure Form Builder

Build forms that respect field permissions:

class SecureFormBuilder extends StatelessWidget { final String entityType; final Map<String, dynamic>? initialData; final Function(Map<String, dynamic>) onSubmit; const SecureFormBuilder({ super.key, required this.entityType, this.initialData, required this.onSubmit, }); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value!; final userPermissions = UserPermissions(currentUser); final attributePermissions = AttributePermissions( currentUser, userPermissions.role, ); return FormBuilder( entityType: entityType, initialData: initialData, fieldBuilder: (fieldName, fieldData) { final canRead = attributePermissions.canReadField( entityType, fieldName, initialData ?? {}, ); final canWrite = attributePermissions.canWriteField( entityType, fieldName, initialData ?? {}, ); if (!canRead) return const SizedBox.shrink(); return FormField( name: fieldName, data: fieldData, enabled: canWrite, readOnly: !canWrite, ); }, onSubmit: onSubmit, ); } } class FormBuilder extends StatefulWidget { final String entityType; final Map<String, dynamic>? initialData; final Widget Function(String fieldName, dynamic fieldData) fieldBuilder; final Function(Map<String, dynamic>) onSubmit; const FormBuilder({ super.key, required this.entityType, this.initialData, required this.fieldBuilder, required this.onSubmit, }); @override State<FormBuilder> createState() => _FormBuilderState(); } class _FormBuilderState extends State<FormBuilder> { final _formKey = GlobalKey<FormState>(); final Map<String, dynamic> _formData = {}; @override void initState() { super.initState(); _formData.addAll(widget.initialData ?? {}); } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ // Build form fields based on permissions ...(_getFieldSchema()[widget.entityType] as Map<String, dynamic>) .entries .map((entry) => widget.fieldBuilder(entry.key, entry.value)), const SizedBox(height: 24), ElevatedButton( onPressed: _submitForm, child: Text(widget.initialData != null ? 'Update' : 'Create'), ), ], ), ); } Map<String, dynamic> _getFieldSchema() { // Define your entity field schemas here return { 'posts': { 'title': {'type': 'string', 'required': true}, 'content': {'type': 'text', 'required': true}, 'tags': {'type': 'array'}, 'status': {'type': 'select', 'options': ['draft', 'published']}, }, 'users': { 'name': {'type': 'string', 'required': true}, 'email': {'type': 'email', 'required': true}, 'role': {'type': 'select', 'options': ['admin', 'editor', 'viewer']}, 'bio': {'type': 'text'}, }, }; } void _submitForm() { if (_formKey.currentState!.validate()) { widget.onSubmit(_formData); } } }

Permission Middleware

Query-Level Permissions

Apply permissions at the query level:

class PermissionMiddleware { final AuthUser currentUser; final UserPermissions permissions; PermissionMiddleware(this.currentUser, this.permissions); Map<String, dynamic> applyReadPermissions(Map<String, dynamic> query) { final modifiedQuery = Map<String, dynamic>.from(query); modifiedQuery.forEach((entityType, querySpec) { if (!permissions.canRead(entityType)) { // Remove entities user can't read modifiedQuery.remove(entityType); return; } // Add ownership filters for private data final spec = querySpec as Map<String, dynamic>; final where = spec['where'] as Map<String, dynamic>? ?? {}; switch (entityType) { case 'private_posts': where['authorId'] = currentUser.id; break; case 'team_documents': where['teamMembers'] = {'\$contains': currentUser.id}; break; } if (where.isNotEmpty) { spec['where'] = where; } }); return modifiedQuery; } List<Operation> applyWritePermissions(List<Operation> operations) { return operations.where((op) { switch (op.type) { case 'create': return permissions.canCreate(op.entityType); case 'update': return permissions.canUpdate(op.entityType, op.data); case 'delete': return permissions.canDelete(op.entityType, op.data); default: return false; } }).toList(); } } // Secure database wrapper class SecureInstantDB { final InstantDB _db; final PermissionMiddleware _middleware; SecureInstantDB(this._db) : _middleware = PermissionMiddleware( _db.auth.currentUser.value!, UserPermissions(_db.auth.currentUser.value!), ); Signal<QueryResult> subscribeQuery(Map<String, dynamic> query) { final secureQuery = _middleware.applyReadPermissions(query); return _db.subscribeQuery(secureQuery); } Future<TransactionResult> transact(List<Operation> operations) async { final allowedOperations = _middleware.applyWritePermissions(operations); if (allowedOperations.isEmpty) { throw InstantException( message: 'No operations allowed for current user', code: 'permission_denied', ); } return await _db.transact(allowedOperations); } }

Advanced Permissions

Dynamic Permission Rules

Implement context-aware permissions:

class DynamicPermissions { final AuthUser currentUser; final DateTime currentTime; final String userLocation; DynamicPermissions({ required this.currentUser, required this.currentTime, required this.userLocation, }); bool canAccess(String resource, Map<String, dynamic> context) { // Time-based permissions if (_hasTimeRestriction(resource)) { if (!_isWithinAllowedTime(resource)) return false; } // Location-based permissions if (_hasLocationRestriction(resource)) { if (!_isInAllowedLocation(resource)) return false; } // Context-based permissions if (_hasContextRestriction(resource)) { if (!_contextAllows(resource, context)) return false; } return true; } bool _hasTimeRestriction(String resource) { const timeRestrictedResources = {'admin_panel', 'financial_reports'}; return timeRestrictedResources.contains(resource); } bool _isWithinAllowedTime(String resource) { // Example: Admin panel only accessible during business hours if (resource == 'admin_panel') { final hour = currentTime.hour; return hour >= 9 && hour <= 17; // 9 AM to 5 PM } return true; } bool _hasLocationRestriction(String resource) { const locationRestrictedResources = {'sensitive_data'}; return locationRestrictedResources.contains(resource); } bool _isInAllowedLocation(String resource) { // Example: Sensitive data only accessible from office locations const allowedLocations = ['office_ny', 'office_sf']; return allowedLocations.contains(userLocation); } bool _hasContextRestriction(String resource) { return true; // Most resources have context restrictions } bool _contextAllows(String resource, Map<String, dynamic> context) { switch (resource) { case 'edit_post': // Can edit if owner or if granted explicit permission return context['ownerId'] == currentUser.id || context['editors']?.contains(currentUser.id) == true; case 'view_analytics': // Can view analytics if admin or if data relates to user's projects final userRole = currentUser.metadata?['role']; if (userRole == 'admin') return true; final projectIds = context['projectIds'] as List?; final userProjects = currentUser.metadata?['projects'] as List?; return projectIds?.any((id) => userProjects?.contains(id) == true) == true; default: return true; } } }

Best Practices

1. Fail-Safe Defaults

Always default to denying access:

class SecureByDefault { static bool checkPermission(String action, {required bool explicit}) { // Require explicit permission grants return explicit; } static List<T> filterByAccess<T>( List<T> items, bool Function(T item) canAccess, ) { return items.where(canAccess).toList(); } }

2. Audit Permission Changes

Log all permission-related activities:

class PermissionAudit { static void logPermissionChange({ required String userId, required String action, required String resource, required bool granted, }) { final logEntry = { 'timestamp': DateTime.now().millisecondsSinceEpoch, 'userId': userId, 'action': action, 'resource': resource, 'granted': granted, }; print('Permission audit: $logEntry'); // Send to logging service } }

3. Test Permission Logic

Thoroughly test permission scenarios:

void testPermissions() { final adminUser = AuthUser.test(role: 'admin'); final editorUser = AuthUser.test(role: 'editor'); final viewerUser = AuthUser.test(role: 'viewer'); final adminPerms = UserPermissions(adminUser); final editorPerms = UserPermissions(editorUser); final viewerPerms = UserPermissions(viewerUser); // Test admin permissions assert(adminPerms.canDelete('posts')); assert(adminPerms.canManageUsers()); // Test editor permissions assert(editorPerms.canWrite); assert(!editorPerms.canManageUsers()); // Test viewer permissions assert(!viewerPerms.canWrite); assert(viewerPerms.canRead('posts')); }

4. Handle Permission Errors Gracefully

Provide clear feedback for permission issues:

class PermissionErrorHandler { static void handlePermissionError( BuildContext context, String action, String resource, ) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Access Denied'), content: Text( 'You don\'t have permission to $action $resource. ' 'Contact your administrator if you need access.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('OK'), ), TextButton( onPressed: () => _requestPermission(context, action, resource), child: const Text('Request Access'), ), ], ), ); } static void _requestPermission( BuildContext context, String action, String resource, ) { // Implement permission request logic Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Permission request sent to administrators'), ), ); } }

Next Steps

Learn more about authentication and security: