Skip to content
Kien

A permission system where one table guards every route

Permissions for a corporate expense-management product — a catalog read from the database, per-enterprise disabling, dependencies with transitive auto-grant — and a frontend where routes, sidebar and buttons all read from one source.

Period
2026
Role
Built it across backend and frontend — catalog, rules, admin screens, guards

Stack

  • NestJS 10
  • Fastify
  • PostgreSQL
  • TypeORM
  • React 19
  • TypeScript
  • Vitest

The problem

EMS is a corporate expense-management system sold to many enterprises at once. Each one wants a different shape of access, and some permissions only make sense together with others. Role checks hard-coded screen by screen drift apart the moment a new screen ships.

The approach

The permission catalog lives in the database, not in code, and an enterprise can switch individual permissions off for itself.

Permissions declare their dependencies. Granting one auto-grants what it needs, transitively; downgrading a permission that others depend on is blocked instead of silently leaving a broken role behind.

The admin side has role, assignment and matrix screens — the matrix drawn as a tree by dependency — plus a "view as user" screen to see exactly what someone else sees.

On the frontend, route guarding and the sidebar both read from one shared ROUTE_ACCESS table, so a page cannot be reachable while its menu entry is hidden, or the other way round. Individual buttons are guarded through a useCan hook rather than ad-hoc checks.

The result

About 5,490 frontend tests green. A permission map covering roughly 300 endpoints is prepared for backend enforcement — written down and ready, so switching the server side on is a deliberate step rather than a rewrite.