Twilight
← Back to all posts

Using Bitfields for Permissions and Feature Flags

September 3, 2025
6 min read

Introduction

Bitfields are a powerful technique for managing permissions and feature flags efficiently. I learned about bitfields since the beginning of my career back in late 2018, thanks to Discord's permissions documentation.

Instead of storing multiple boolean values or arrays, bitfields pack multiple flags into a single number using bitwise operations. This approach is memory-efficient, fast, and perfect for systems that need to check many permissions or features quickly.

How Bitfields Work

A bitfield uses individual bits in a number to represent different flags. Each bit position represents a specific permission or feature:

Bit Position76543210
Binary10101101

In this example the decimal value 173 is represented as 10101101 in binary. Bits 0, 2, 3, 5, and 7 are set (1), while bits 1, 4, and 6 are unset (0).

Logic Gates and Bitwise Operations

Bitfields work using basic logic gates. Here's how each one works with truth tables:

AND (&) - Checks if a bit is set

The AND operation returns 1 only when both inputs are 1:

ABA & B
000
010
100
111

OR (|) - Sets a bit

The OR operation returns 1 when at least one input is 1:

ABA | B
000
011
101
111

XOR (^) - Toggles a bit

The XOR operation returns 1 when inputs are different:

ABA ^ B
000
011
101
110

NOT (~) - Flips all bits

The NOT operation flips each bit (0 becomes 1, 1 becomes 0):

A~A
01
10

Programmatically you can use the following operations:

// Example with 8-bit number: 10101101 (173)
const value = 173n; // Binary: 10101101
 
// Check if bit 2 is set (AND operation)
const isBit2Set = (value & (1n << 2n)) !== 0n; // true
 
// Set bit 1 (OR operation)
const setBit1 = value | (1n << 1n); // 175n (10101111)
 
// Toggle bit 0 (XOR operation)
const toggleBit0 = value ^ (1n << 0n); // 172n (10101100)
 
// remove bit 2
const removeBit2 = value & ~(1n << 2n); // 169n (10101001)

Basic BitField Implementation

Here's a simple BitField class for managing permissions:

class BitField {
  private value: bigint;
 
  constructor(value: bigint | string | number = 0n) {
    this.value = BigInt(value);
  }
 
  // Add a permission (set bit)
  add(permission: bigint): this {
    this.value |= (1n << permission);
    return this;
  }
 
  // Remove a permission (clear bit)
  remove(permission: bigint): this {
    this.value &= ~(1n << permission);
    return this;
  }
 
  // Toggle a permission (flip bit)
  toggle(permission: bigint): this {
    this.value ^= (1n << permission);
    return this;
  }
 
  // Check if permission exists (check bit)
  has(permission: bigint): boolean {
    return (this.value & (1n << permission)) !== 0n;
  }
 
  // Check if all permissions exist
  hasAll(permissions: bigint[]): boolean {
    return permissions.every(permission => this.has(permission));
  }
 
  // Check if any permission exists
  hasSome(permissions: bigint[]): boolean {
    return permissions.some(permission => this.has(permission));
  }
 
  // Get all set permissions
  toArray(): bigint[] {
    const permissions: bigint[] = [];
    let temp = this.value;
    let position = 0n;
 
    while (temp > 0n) {
      if (temp & 1n) {
        permissions.push(position);
      }
      temp >>= 1n;
      position++;
    }
 
    return permissions;
  }
 
  // Serialize to string for storage/transfer
  toString(): string {
    return this.value.toString();
  }
 
  // Create from string
  static fromString(value: string): BitField {
    return new BitField(value);
  }
}

Permission System Example

// Define permission constants using bit shifts
const PERMISSIONS = {
  READ: 1n << 0n,      // 1n
  WRITE: 1n << 1n,     // 2n
  DELETE: 1n << 2n,    // 4n
  ADMIN: 1n << 3n,     // 8n
  MODERATE: 1n << 4n,  // 16n
} as const;
 
// Create user permissions
const userPermissions = new BitField()
  .add(PERMISSIONS.READ)
  .add(PERMISSIONS.WRITE);
 
// Check permissions
console.log(userPermissions.has(PERMISSIONS.READ));  // true
console.log(userPermissions.has(PERMISSIONS.DELETE)); // false
 
// Add admin permission
userPermissions.add(PERMISSIONS.ADMIN);
 
// Check multiple permissions
console.log(userPermissions.hasAll([PERMISSIONS.READ, PERMISSIONS.WRITE])); // true
 
// Serialize for database storage
const serialized = userPermissions.toString(); // "11"

Feature Flags Example

const FEATURES = {
  DARK_MODE: 1n << 0n,
  PREMIUM_CONTENT: 1n << 1n,
  BETA_FEATURES: 1n << 2n,
  ANALYTICS: 1n << 3n,
  NOTIFICATIONS: 1n << 4n,
} as const;
 
class User {
  private features: BitField;
 
  constructor(features: string = "0") {
    this.features = BitField.fromString(features);
  }
 
  enableFeature(feature: bigint): void {
    this.features.add(feature);
  }
 
  disableFeature(feature: bigint): void {
    this.features.remove(feature);
  }
 
  hasFeature(feature: bigint): boolean {
    return this.features.has(feature);
  }
 
  getFeatures(): string {
    return this.features.toString();
  }
}
 
// Usage
const user = new User();
user.enableFeature(FEATURES.DARK_MODE);
user.enableFeature(FEATURES.PREMIUM_CONTENT);
 
console.log(user.hasFeature(FEATURES.DARK_MODE)); // true
console.log(user.getFeatures()); // "3"

Advantages and Disadvantages

Advantages

AdvantageDescription
Memory EfficientSingle number instead of multiple booleans
Fast OperationsBitwise operations are extremely fast
Easy SerializationSingle value to store/transfer
Atomic OperationsAll flags updated together

Disadvantages

DisadvantageDescription
Not Human ReadableRequires bit manipulation knowledge
Debugging DifficultyHard to understand raw values
Memory UsageCan use significant memory with many flags

When to Use Bitfields

Use bitfields when:

  • You have many boolean flags (10+)
  • Performance is critical
  • You need atomic flag operations
  • Memory usage matters
  • Flags are frequently checked together

Don't use bitfields when:

  • You have few flags (< 5)
  • Flags have complex relationships
  • You need human-readable configuration
  • Flags change frequently during runtime

Important Notes

Always use bigint type to declare bitfields and serialize it to a string to store it or transfer it over the wire because JavaScript numbers are limited to 32-bit bitwise operations.

// ❌ Wrong - limited to 32-bit operations
const permissions: number = 1 << 32; // 1 (wraps around)
 
// ✅ Correct - use bigint for unlimited precision
const permissions: bigint = 1n << 32n; // 4294967296n (exact)

Conclusion

Bitfields are a powerful tool for managing permissions and feature flags efficiently. They provide excellent performance and memory usage at the cost of some readability. Use them when you need to manage many boolean flags and performance is important.

The key is to use bigint for large bitfields, serialize to strings for storage, and use bit shift operations (1 << n) instead of magic numbers for better maintainability.