Day#8 📊 Designing a Scalable Permission Model
Yesterday I took a break. Today I’m back – and permissions are under control, I hope.
I’ve implemented a structured permission model that allows for precise access control across the system.
Instead of hardcoding logic for each route, I rely on a table-driven model that supports dynamic evaluation at runtime.
🧱 The Model
Permissions are defined per module, per operation (create
, read
, update
, delete
), and per scope (self
, group
, global
).
With 8 modules × 4 operations × 3 scopes, that gives:
8 × 4 × 3 = 96 unique permissions
These permissions are stored in the table:
CREATE TABLE users.system_role_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module TEXT NOT NULL,
operation TEXT NOT NULL,
scope TEXT NOT NULL CHECK (scope IN ('self', 'group', 'global')),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
Example entry:
INSERT INTO users.system_role_permissions (module, operation, scope)
VALUES ('user', 'update', 'self');
🔐 Role Composition
Rather than assign all 96 permissions manually, I grouped them into logical system roles:
Role | Description | Permissions Count |
---|---|---|
admin | Full access to everything | 32 |
supervisor | Full access in group, self access | 32 |
employee | Read and update only own resources | 16 |
Total bindings: 32 + 32 + 16 = 80
These bindings are stored in:
CREATE TABLE users.system_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT
);
CREATE TABLE users.system_role_permission_bindings (
role_id UUID NOT NULL REFERENCES users.system_roles(id),
permission_id UUID NOT NULL REFERENCES users.system_role_permissions(id),
PRIMARY KEY (role_id, permission_id)
);
👤 Test Account
To test the setup, I inserted a test user with admin privileges:
INSERT INTO users.users (
id, email, first_name, last_name, password_hash, created_by, updated_by
) VALUES (
'00000000-0000-0000-0000-000000000000',
'admin@example.com',
'Admin',
'User',
'$2b$10$2mFTr8P4y3uEtdjKhk7w/.QnAVB/0TK08bhfs3h3UUmwD5tNnpz3C', -- password: admin <- who could even guess? ;)
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000'
);
🔍 Who Has What?
To inspect user-role relations, I created a view:
CREATE VIEW users.v_user_roles AS
SELECT
r.id AS role_id,
r.name AS role_name,
p.id AS user_id,
p.email,
p.first_name,
p.last_name
FROM users.system_roles r
JOIN users.user_system_roles psr ON psr.system_role_id = r.id
JOIN users.users p ON psr.user_id = p.id;
Query example:
SELECT * FROM users.v_user_roles WHERE email = 'admin@example.com';
✅ Summary
The model is simple to reason about, supports dynamic enforcement, and cleanly separates responsibilities. It also scales well with future modules, operations, or scopes.
Next step: group-based dynamic permissions via group_memberships
.