Most access control systems are designed for one organisation. Ours had to work for 10+ and each with a different internal structure, different compliance requirements, and different ideas about who should be able to do what.

I've built role-based access control systems before. The usual approach is straightforward: define a handful of roles, assign users to them, check permissions at the route level. Done.

This project was different. We were building a multi-tenant SaaS platform for a Singapore-based fintech company in the pawnbroking space, and the access control problem turned out to be one of the most interesting design challenges on the whole project.

This is how we thought through it.

Why a Standard Role System Wasn't Going to Work

The platform was designed to be licensed to multiple pawnbroking businesses. Each tenant is a separate organisation with its own staff, its own internal hierarchy, and its own operational structure. Tenant A might run with seven roles. Tenant B might need eighteen. Neither of them looks like the other.

If we hardcoded roles into the platform, every new tenant would need the platform team to configure access for them. That's not a product, that's a support contract. We needed tenant admins to own their access structure completely, without touching the platform layer.

At the same time, compliance was non-negotiable. Certain operations, creating a pledge above a value threshold, processing a loan redemption, amending a transaction, needed approval before they could execute. And those approval chains varied by tenant too.

Everything had to be configurable. Nothing could be hardcoded.

The Permission Model

We structured permissions as a hierarchy: features, modules, and access controls.

Features are the smallest unit. Each feature maps to a specific action in the application, viewing a page, triggering an operation, accessing a button. Features are defined in code. The total set of possible features is finite and known at any point in time, so there was no reason to make this table dynamic. We created a static, read-only features table.

That was a deliberate choice. Adding a new permission to a live system should be a deliberate act, a code change and a migration. Not something that happens accidentally through an admin interface.

Modules group features together. The platform ended up with 150+ individual permissions. A flat list of 150 checkboxes is unusable for a tenant admin trying to configure a role. Grouped by module, it becomes navigable. If a role needs all twenty features in the audit module, the admin selects the module in one click. The UX decision and the data model decision were the same decision.

The menu system is also driven by this structure. If a role includes no features from a given module, that module doesn't appear in the navigation at all. Permissions aren't just enforced at the API level, they shape what the user sees.

Making Roles Configurable Without Making Them Chaotic

Roles are built on top of the permission model. Each role is a named collection of features, configured by the tenant admin.

The tenant admin sees the full access control list organised by module. They can select individual features or select an entire module at once. Once a role is saved, it can be assigned to users.

The important constraint: tenant admins can configure any combination of features into a role, but they cannot create new features or change what a feature does. That boundary is enforced at the data model level. Tenant admins have write access to the roles table and the user-role mapping table. The features and modules tables are read-only from their side.

This gives them real control without giving them control over things that could break the platform.

Why We Allowed Multiple Roles Per User

One role per user was not enough. We figured this out during discovery, and it shaped one of the more interesting data model decisions on the project.

Here's the scenario that kept coming up. A finance staff member has the finance manager role. The tenant admin needs this person to temporarily access audit-related information for a compliance task. Without multi-role support, there are two options, and both are bad.

Option one: add audit permissions to the finance manager role. This gives every finance manager audit access permanently. Not the intent.

Option two: create a brand new custom role for this specific combination of permissions. This works once. But every unique access requirement generates a new role. The roles table fills up with one-off roles that nobody else uses, and the tenant admin loses the ability to understand what any role actually means.

With multi-role, the admin assigns the audit analyst role to that user for the duration of the task. When it's done, they remove it. Clean, reversible, no side effects.

The data model is a user-role mapping table with a composite primary key on user ID and role ID. At query time, the system unions the feature sets across all of a user's roles. Permission checks run against the combined set.

The Approval Workflow Design

Certain operations require approval before they execute. Pledge creation above a value threshold, loan renewals, redemptions, amendments, around 15 distinct scenarios, each with different conditions and different approval requirements.

Not every scenario needed multi-level approval. Some needed one level, some two. The configuration lived in the master data settings in the head office portal. Tenant admins defined, for each scenario, how many approval levels were required and which roles were eligible to approve at each level.

The key decision was role-based approver assignment rather than user-based. The tenant admin doesn't pick specific people to approve, they pick roles. When an approval is triggered at runtime, the system identifies users who currently hold the eligible role, then assigns the approval based on a set of parameters: proximity to the branch, current availability, and round-robin as a tiebreaker.

The flow is synchronous. Level one must complete before the request moves to level two. The transaction is blocked until the full chain resolves.

This design meant that tenant admins never had to update approval configuration when staff changed. As long as the right roles exist and users are assigned to them, the system figures out who approves what.

Audit Logging That Actually Supports Compliance

Generic application logs are too broad for compliance purposes. They tell you something happened. A compliance audit needs to know exactly what changed, who changed it, and when, with before and after state.

We used separate audit log tables per approval scenario type rather than a single generic audit table. Pledge creation approvals go in one table. Renewal approvals in another. Redemption approvals in another. Each table has a defined schema that captures the full relevant context for that scenario: transaction data, approver at each level, remarks, timestamps, user identifiers.

The reason for separate tables over a generic JSON blob approach was precision. A flat audit table with unstructured change data is hard to query and harder to present during a regulatory inspection. Separate tables with defined schemas mean the data is structured, queryable, and directly readable without interpretation.

Because the platform uses schema-level tenant isolation, each tenant's audit logs sit in their own schema. Pulling a single tenant's audit history is a straightforward query, no cross-tenant filtering needed, no risk of data leakage.

What I'd Do Differently

The features and modules table structure has a structural limitation that's fine at current scale but would get uncomfortable at 100 tenants.

The features table is static and shared across all tenants. Features represent UI-level actions, viewing a page, triggering an operation, accessing a button. Adding a new feature means a code change and a migration. At current scale with a stable, known set of screens, that's manageable. At 100 tenants with more diverse workflow requirements, the pressure to add new features increases and the migration-per-feature model becomes slower.

The deeper issue is tight coupling between features and modules. Each feature has a foreign key to a module, and features map directly to the access controls layer through a role-feature mapping table. Any restructuring of modules, splitting a large one, reorganising features across modules, requires careful migration work across multiple tables. There's no clean way to do it without risk of breaking existing role configurations for live tenants.

The other gap is role change history. The current system logs what users do with their permissions. It doesn't log when a tenant admin modifies a role. For a compliance-heavy platform, that's an audit trail that should probably exist.

Neither of these is a problem at current scale. But they're the things I'd design differently from the start.

Where It Stands

The platform supports 10+ active tenants with around 10,000 daily active users. Each tenant manages their own role structure through the admin portal. The platform team has not had to manually configure access for any tenant since launch.

The 15 approval scenarios cover the most compliance-sensitive operations in the workflow. Every one is logged, structured by scenario type, and queryable without engineering involvement. The client runs compliance reviews directly against the audit tables.

The multi-role system kept the roles table clean. Instead of creating one-off roles for edge cases, tenant admins combine existing roles per user. The roles table stays meaningful.

One Thing Worth Remembering

The access control problem on this project wasn't really a technical problem. It was an organisational one. Every tenant has a different internal structure, and the system had to accommodate all of them without knowing what any of them looked like in advance.

The solution wasn't clever. It was the right level of flexibility in the right places, static where things should be stable, configurable where businesses actually differ, and logged everywhere that compliance requires it.

That balance is most of the design work.

Bye & Happy Coding…!

If you want to talk through architecture decisions like these, feel free to reach out.

Email: surajbiswas367@gmail.com

LinkedIn: https://www.linkedin.com/in/iamsurajdev/