How to implement Role-Based Access Control using binary bitmasks — encoding roles, permissions, and combinations as efficient binary flags.
Role-Based Access Control (RBAC) is a standard approach to managing permissions in applications. Instead of assigning permissions to individual users, you assign permissions to roles and then assign users to those roles. The bitmask variant of RBAC encodes all permissions for a role as bit positions in a single integer. I have used this pattern in everything from small web applications to enterprise SaaS platforms, and it consistently delivers the best performance-to-complexity ratio.
The fundamental idea is simple: every permission in your system is assigned a unique bit position in an integer. A role's permission set is the bitwise OR of all its individual permissions. A user's effective permission set is the bitwise OR of all their roles' permission sets. Checking whether a user can perform an action is a single bitwise AND comparison.
After struggling with a 12-table RBAC schema in a previous job, I rebuilt it using bitmasked roles stored in a single integer. The permission check went from 200ms with joins to under 1ms.
The key advantage over a relational permission system is speed: there are no JOIN queries, no pivot tables, and no string comparisons. Every permission check is a single integer AND operation that executes in one CPU cycle. In high-traffic applications I have worked on, this has made a measurable difference in API response times.
Always design your bit layout before implementing. Reserve bits 0-7 for basic CRUD, bits 8-15 for administrative functions, bits 16-23 for feature-specific permissions, and bits 24-31 for future expansion. Use our bitwise calculator to visualize your permission assignments as you design them.
The most important design decision in a bitmask RBAC system is how you assign bit positions. I have learned through experience that a well-organized bit layout prevents conflicts and makes the system easier to audit. Here is a recommended layout pattern:
| Bit Range | Category | Example Permissions |
|---|---|---|
| 0-4 | Basic CRUD | READ, WRITE, DELETE, CREATE, EXPORT |
| 5-9 | Content Operations | PUBLISH, ARCHIVE, REVIEW, APPROVE, REJECT |
| 10-15 | Administrative | MANAGE_USERS, MANAGE_ROLES, VIEW_LOGS, CONFIG, AUDIT |
| 16-23 | Feature-Specific | API_ACCESS, REPORTING, IMPORT, INTEGRATIONS, BILLING |
| 24-31 | Reserved | Future expansion, SUPER_ADMIN, SYSTEM_OVERRIDE |
Here is the concrete bit assignment I currently use in a production application:
The rule I follow is: never assign bit positions sequentially across categories. Always leave gaps (skip bit 9 between content and admin, for example). This gives you room to insert new permissions later without reshuffling the entire layout. Reshuffling bit positions in a production system is painful because it requires updating every stored permission mask.
In a bitmask RBAC system, a user's effective permissions are the bitwise OR of all roles assigned to them. This accumulation model is simple and correct: if any role grants a permission, the user has that permission.
One edge case I have encountered: when a user has multiple roles and one role explicitly does NOT grant a permission, that does not revoke the permission from another role. In bitmask RBAC, there is no "negative permission" — OR only adds bits, never removes them. This is by design. If you need to revoke a specific permission from a user who has multiple roles, you must remove the role that grants it or use a separate deny mechanism.
Role inheritance — where an admin role automatically includes all editor permissions — is elegantly handled with bitmasks. Instead of building a role hierarchy table with parent-child relationships, you simply use bitwise comparisons.
The pattern is: if role_a.mask & role_b.mask === role_b.mask, then role A has all permissions that role B has. In other words, role A's mask is a superset of role B's mask.
This superset check is useful for many things beyond role hierarchy. For example, you can check whether a user has "at least" a certain level of access. If the minimum permission set for using the admin panel is READ | WRITE | ADMIN (binary 1011), any user whose effective mask ANDed with this requirement equals the requirement should be allowed in.
Here is a complete, production-tested implementation of bitmask RBAC in JavaScript. I have used variations of this code in Node.js, PHP, and Python projects. The core logic — bitwise operations — is identical across languages.
One practical lesson from deploying this in production: always use UNSIGNED INT in MySQL (or BIGINT if you need more than 32 bits). Signed integers can cause unexpected behavior with the high bit (bit 31) because it becomes the sign bit. In MySQL, INT UNSIGNED gives you all 32 bits cleanly. In PostgreSQL, use INTEGER with explicit bit operations.
Use the bitwise calculator to plan your permission bit layout, test role combinations, and verify that your superset inheritance works correctly.
An RBAC bitmask encodes Role-Based Access Control permissions as binary flags in an integer. Each permission (read, write, delete, admin, etc.) is assigned a unique power-of-2 bit position. Permissions are combined using bitwise OR, and checked using bitwise AND. This allows multiple permissions to be stored and checked in a single integer value with CPU-level efficiency.
Design RBAC permission bits by listing all distinct permissions in your system and assigning each one a power-of-2 value: READ=1, WRITE=2, DELETE=4, ADMIN=8, EXPORT=16, etc. Document these in a single place, never reuse bit positions, and leave room between groups (e.g., bits 0-7 for basic CRUD, bits 8-15 for admin functions, bits 16-23 for feature-specific permissions).
To combine multiple roles in bitmask RBAC, OR the permission masks of all roles together. For example, if an editor role has READ|WRITE=3 and a moderator role has READ|DELETE=9, a user in both roles gets 3|9=11 (binary 1011), which grants READ, WRITE, and DELETE. This is called an accumulation strategy and is the simplest way to handle multiple roles.
A 32-bit integer can store up to 32 independent permissions (bits 0-31). A 64-bit integer can store up to 64. For most applications, 32 permissions are sufficient. If you exceed 32, consider using a 64-bit BIGINT or splitting permissions into multiple bitmask columns (e.g., a 32-bit mask for CRUD permissions and a separate 32-bit mask for administrative permissions).
Bitmask RBAC offers significant performance advantages: permission checks are a single CPU-level AND instruction instead of a multi-join SQL query; bitmasks can be cached in a single integer field alongside the user record; no separate permission pivot table is needed; and inheritance (e.g., admin includes all lower permissions) can be implemented with simple bitwise comparisons rather than complex SQL. The tradeoff is reduced flexibility for ad-hoc permission changes.
In bitmask RBAC, permissions can only be added (via OR), not removed, from a role definition. To revoke a specific permission, you must define a new role without that permission, or use a deny-override strategy where a separate "blocked permissions" mask is ANDed with the user's effective mask. The formula becomes: effective = (role_mask_accumulation) & ~blocked_mask.
Yes, PostgreSQL supports all bitwise operators (&, |, ~, <<, >>) and the BIT_OR aggregate function. Use INTEGER or BIGINT columns to store permission masks. PostgreSQL also supports bit string types (BIT VARYING) if you prefer to work with binary string literals like B'1011', but integer types are more portable across databases and application languages.