When customers started reporting degraded performance in parts of our app, I dug into the issue and traced it back to our permission handling logic.
The problem
Our app’s permission system was structured as follows:
- Permissions: an component with a name and a boolean.
- Role: an entity with a name and a repeatable component
permissions(user based). - Module: an entity with a name and a repeatable component
permissions(company based).
Every time the backend needed to verify access, the getPermissions function was triggered. It checked:
- Does the user’s role have this permission?
- Does the company’s module also grant this permission?
Only if both were true, the request could continue.
This meant we had to run deep populates (relation-heavy queries) for both role and module. As the number of permissions grew, performance degraded significantly — some queries were taking 200+ ms.
{
"id": 1,
"username": "john_doe",
"role": {
"name": "Manager",
"permissions": [
{ "name": "read", "allowed": true },
{ "name": "write", "allowed": false }
]
},
"company": {
"id": 1,
"modules": [
{
"key": "projects",
"permissions": [
{ "name": "read", "allowed": true },
{ "name": "write", "allowed": false }
]
}
]
}
}
The optimisation.
To solve this we extended both role and module with a JSON field.
Using StrapiCMS lifecycles, we precompute all role and module permissions whenever they are created or updated. This results in a flattened JSON object that contains all allowed actions per module.
- Instead of: populate: '*' → multiple joins on role + module permissions.
- Now: Directly fetch a single JSON object and check access instantly.
This cut out the need for repeated relational lookups, making getPermissions a lightweight JSON check.
The results
After removing the deep populates and reading directly from JSON: 241 ms → 3.7 ms.
A 65x improvement in response time.
What’s Next? (Still Room for Improvement)
While this optimisation fixed the performance bottleneck, the current approach is not the ideal setup yet. We could still improve by:
- Adding heavy caching on permissions For example, storing effective permissions in Redis or an in-memory cache so that getPermissions rarely hits the database.
- Rewriting how permissions are handled entirely