Where I actually learned bitwise operators

Bitwise operators looked like trivia to me until I started writing Discord bots back in 2019. Every permission in Discord (kick members, manage channels, view audit log) is a single bit in an integer, and checking whether a role can do something is one & against a number. No array, no join table, no .includes().
I liked it enough to reach for the same pattern in my own RBAC code: one permissions column, a set of bit constants, done.
The trick
const Permission = {
READ: 1 << 0,
WRITE: 1 << 1,
DELETE: 1 << 2
} as const;
const userPerms = Permission.READ | Permission.WRITE;
const canDelete = (userPerms & Permission.DELETE) === Permission.DELETE; // falseFor a small, fixed set of roles this is genuinely simpler than the relational version:
- One integer instead of a table. No row per permission per user.
- One comparison instead of a query. The check is arithmetic, not I/O.
- One value to carry around. It fits in a token claim with nothing to join against.
It is the kind of trick that looks clever in a demo and stays clever in production. Mostly.
The ceiling
The "mostly" is the part worth knowing before you build on it. JavaScript's &, |, and ^ convert both operands to 32-bit signed integers before doing anything. 1 << 31 flips the sign bit and becomes a negative number, and past that point the operators stop meaning what you think they mean.
Thirty-one usable bits is the actual ceiling. Not thirty-two, and not "however many bits fit in a safe integer." You can claw bit 31 back by reading the result as unsigned with >>> 0, but then every call site has to remember to, and the one that forgets hands you a negative number instead of a permission.
Discord hit it first
Its own permissions documentation lists permission bits up in the 1 << 52 range now, BYPASS_SLOWMODE among them, and states outright that in the deprecated v6 API the permission integers "shall not grow beyond 31 bits." That is why newer permissions had to ship in separate _new fields until the whole system moved to string-serialized values and BigInt.
A bitfield is a bet that the permission set stays small. The people who wrote the spec lost that bet on their own product.
Building it today
Skip plain numbers. Start with BigInt constants (1n << 40n, and so on) the moment the permission set is not provably small and fixed forever. The bitwise operators work bit for bit with no truncation, at the cost of heavier syntax and no implicit number coercion.
BigInt does not serialize either. JSON.stringify throws on it, so anything crossing a wire or landing in a token claim has to travel as a string. That is where Discord ended up, and it is worth copying deliberately rather than rediscovering once the payloads are already out in the world.
The language is only half the ceiling. The column has one too. Postgres bigint is 64 bits signed, so storage tops out at 63 usable flags no matter how careful the application code is, and an integer column caps you at 31 whether or not that code ever sees a truncated number. Pick the wide column before you need the wide constants. Migrating a numeric bitfield that already sits in a database and in a few thousand JWTs is a much worse day than typing an extra n up front.
Thirty-one roles is a lot for most internal admin panels. It is not a lot for a permission system a product team gets to keep adding to.