At 9fin, we build AI-powered intelligence for debt markets. Our users are analysts and investors making high-stakes decisions using data that is often confidential, commercially sensitive and governed by access rules that vary by client, deal and document. If the wrong user sees the wrong document, that is not just a bug, it is a breach of trust and potentially a compliance incident.
Confidentiality is non-negotiable, and access control to these documents must be precise, flexible, and correct.
We built our own authorisation system to meet those requirements. Over time, it became one of the hardest parts of our system to operate. This is the story of why building a custom system first was the right call, what it cost us, and how we migrated to a purpose-built authorisation engine without losing the domain clarity we gained along the way.
Why we built a custom authorisation system
Access patterns to the data on the 9fin platform are granular and relational: a user's access depends on their organisation, which data rooms that organisation can reach, which documents sit inside those data rooms, and whether those documents carry a public or private designation. These relationships change over time and vary from client to client.
What is a data room?
In debt markets, private companies release documents to investors through data rooms which are a type of document management solution. These documents are given a Designation which is an indication of how confidential it is.

Critically, when we started, we did not fully understand the access model we would ultimately need. New content types, client structures and product surfaces kept expanding the domain. Committing early to a third-party provider would have meant modelling a fast-moving domain before we properly understood it.
There was another dimension too: we have multiple services sharing the same database. As a result, we couldn’t reasonably stay confident about which applications were reading which rows, without the overhead of reassessing every time a new feature rolled out. Enforcing access at the database layer meant that even if a service we had not fully audited ran a query, it could only see what the policies allowed.
So we made a deliberate choice: build a learning system, with the knowledge we’d need to replace it.
What we built: a relationship-based system enforced in Postgres
We built a Relationship-Based Access Control model enforced by PostgreSQL Row Level Security. Every query against confidential content was filtered by Postgres itself. If the policy said a user could not see a row, that row did not exist in their results.
Using the database as the enforcement boundary eliminated a whole class of application-layer mistakes. "Forgot to add a WHERE clause" stopped being a threat. The relationship model gave us flexibility: when we needed to change the relationships that informed permissions, we extended the model. The barrier to contribution was low enough that more teams could experiment, surfacing edge cases we would never have found in a design document.
Though, the costs compounded as RLS injects hidden predicates into every query touching a protected table. The same query performs very differently depending on a user's relationships, three data rooms vs 300 can trigger completely different query plans. Debugging shifted from application code to policy logic. Schema migrations and new relationship types needed careful testing; a bad policy could make all users see nothing, or worse, an slightly over-permissive policy could go undetected.
Performance degradation was surprisingly non-linear. Batch permission checks that started in milliseconds crept into seconds, then minutes. Engineers became cautious about changing anything near the policies. The system that gave us speed while we built out the system started costing us speed as we needed to scale.
Migrating from Postgres RLS to a dedicated authorisation engine
Our strategy was to migrate once two conditions were met: we understood the domain well enough to model it confidently in a third-party system, and the operational cost of RLS outweighed the learning benefit.
A single integration surface. We migrated every consuming application to use an application side filtering service (which used the same logic as the RLS policies) as their policy decision point (PDP). When we swapped the engine behind the service, no consuming application needed to change.


A dedicated authorisation engine. We chose OpenFGA, an open-source engine based on Google's Zanzibar model. It gave us lightning fast access checks. Operations that took minutes under RLS completed in under a second. OpenFGA natively models the kind of relationship graph we had painstakingly built on top of Postgres: users, organisations, data rooms and documents translated directly.
Relationship mapping. The hardest part was keeping the relationship graph consistent with source-of-truth data. We built an event-driven pipeline, messages about permission-relevant entities were published and consumed into relationship writes, such as a document being linked to a data room or a team being given access to one. This is where the year of domain discovery paid off. We knew which relationships mattered, which edge cases would bite us, and which entity lifecycle events could create inconsistencies. We were not guessing. We were transcribing hard-won knowledge.
Shadow mode. We did not cut over in a single deploy. Every permission check ran against both the existing system and the new engine, with the existing system remaining authoritative. Discrepancies triggered alerts.

Shadow mode ran for weeks. Most mismatches were cases where the new model was more correct than the old policies. A few were genuine bugs in the mapping pipeline, caught before they mattered.
Retiring RLS. Once shadow testing confirmed the new engine matched or improved on RLS enforcement, we flipped feature flags, kept RLS policies in place as a rollback path, monitored, then dropped them.
Why building our own authorisation system first was the right call
The custom system was not a mistake we later corrected. It was a learning investment that made the migration possible.
Without the RLS phase, we would not have known that data room-level permissioning was more correct than instrument-level permissioning as the right operating model. We would not have discovered that batch permission checks for digest emails and search indexing would become the dominant bottleneck. When we modelled access patterns in OpenFGA, we did not need to guess. We had a year of production experience, incident reports and edge cases telling us exactly what the model needed to express.
The custom system was the prototype. OpenFGA is the product.
What we would tell another team
If your requirements are both critical and unclear, building your own authorisation system can be the right call. But only if you treat it as a discovery tool, not a legacy to defend. Write the exit strategy before you write the code. If you cannot describe how you will migrate away, you are not taking on calculated debt. You are just taking on debt.
Sometimes building the wrong system first is the fastest way to build the right one.
If you’re interested in thinking outside of the box to solve difficult problems, we’re hiring and would love to have a chat!






