We’ve been quiet these past months. From the outside, it may even have looked like a lull. Inside, however, we were rebuilding the core of Scanfully so it could carry bigger sites, stricter security requirements, and the collaboration features customers kept asking for.
The work was not glamorous, and it touched every single thing that moves data. To a degree, we did a full rewrite of our entire database storage approach.
Here is what we changed, why it took months, and what it unlocks next, told from the engineering trenches rather than a product brochure.
What We Mean by a Tenant Solution
A tenant‑based (multi‑tenant) system runs one application for many customers while keeping their data completely isolated. Everyone uses the same platform, but no one can see anyone else’s records. The appeal is obvious, efficiency, scale, and shared improvements, but the hard part is getting isolation right in a way that stands up under real load and real mistakes.
In a multi‑tenant system:
- Data Isolation: Each tenant’s data is kept separate and secure.
- Resource Sharing: Multiple tenants share the same application code and database infrastructure.
- Scalability: The system can efficiently serve many customers without duplicating infrastructure.
- Cost Efficiency: Shared resources reduce operational overhead compared to separate instances per customer.
Moving Isolation into the Database
Scanfully started life with a single highly privileged database connection and careful application‑level filtering. That model put too much faith in every line of code doing the right thing every time. As larger sites came on board, the margin for error shrank. The decision was clear: move isolation from “we promise to filter correctly” to “the database refuses to return anything it should not.”
PostgreSQL Row‑Level Security (RLS) became the backbone of the redesign. Instead of teaching every query about tenant boundaries, we taught the database. RLS policies now scope reads and writes to the active tenant automatically. That shift sounds simple; it is not. It required us to rethink schemas, identifiers, request handling, background work, and how the application thinks about privilege.
Structural Changes That Made It Real
The first visible change was the split into two database schemas. An admin schema holds the facts about tenants themselves and the relationships between users and tenants.
The application schema holds the business data Scanfully works with sites, scans and monitors, notifications, analytics, billing, and more. That division lets us manage tenant metadata independently from the operational data it governs, which pays off in audits, migrations, and day‑to‑day reasoning.
Every table that represents business data now carries a strict tenant reference. That meant touching more than twenty tables and making ownership unambiguous: the database enforces it, cleans up consistently when a tenant is removed, and refuses to accept data that does not belong anywhere.
The goal was simple to say and hard to achieve: no record without an owner, no path around the guardrails.
With RLS turned on across those tables, we needed a reliable way to make sure the database always knew which tenant was active. We introduced a tenant context that lives for exactly as long as a request or background job does. A request authenticates the user, resolves which tenant they are acting on, and establishes that context before a single query runs. Every query that follows is automatically scoped.
If something fails mid‑flight, the whole thing rolls back, context and all. The important part is not the mechanics; it is the guarantee that nothing slips through the cracks.
We also separated responsibilities inside the application. Most code now runs in a tenant‑scoped layer that cannot see outside its boundary at all. Administrative operations that genuinely need a wider view, platform‑level analytics, tenant management, operational tooling, run in a privileged layer that is kept small, explicit, and auditable.
A small global layer deals with things that do not belong to any tenant, such as OAuth handshakes and health checks. That split enforces the principle of least privilege in a way the old system simply could not.
Background Work Without Cross‑Talk
Background processing used to be a quiet source of complexity. Jobs that touched many customers had to be careful not to blend results or carry state from one run to the next. Under the new model, background workers iterate through tenants deliberately, establishing tenant context for each one, and recording successes and failures per tenant.
A failure for one customer does not stall the queue or taint the rest. It reads simple on paper; it matters a lot when the queue is busy.
What We Gained Compared to the Old World
The differences are stark when you line them up. Previously, a single privileged connection saw everything; now the database enforces what any given connection is allowed to see. Previously, we relied on careful WHERE clauses and reviewer vigilance; now queries are automatically filtered whether or not a developer remembered to add a condition. Previously, the security story lived in hundreds of places across the codebase; now it lives where auditors expect to find it, in clear policies and constraints.
There is another, quieter benefit: the application code got simpler. When the database guarantees isolation, repositories and services can focus on behavior rather than constantly defending their boundaries.
It makes the system easier to test, easier to reason about, and easier to extend.
How We Migrated Without Breaking Trust
You do not turn on a system like this with a single switch. We built it in layers. First came the new schemas and the tenant registry. Then we threaded tenant ownership through the data model and backfilled historical data. Only after validation did we tighten constraints.
Only after that did we enable RLS and the automatic rules that keep ownership consistent as new data comes in. Finally, we updated the application to carry tenant context through requests and background work, verified behavior under load, and removed the old, risky paths.
The hardest part was sequencing: introducing new rules while customers continued to use the platform, and proving isolation without interrupting service. Transaction boundaries, careful validation, and a lot of rehearsal paid off.
Ready for What Comes Next
This architecture does not just make Scanfully safer; it makes new features possible, and we have a lot of features lined up!
Things like organizational accounts, multiple users per tenant, and role‑based access now also have a natural home in the data model. Audits become more straightforward. Performance scales more predictably because isolation rules are centralized instead of scattered through the code.
Most importantly, the responsibility for isolation now sits where it belongs. We no longer ask every part of the application to remember what it is allowed to see. The database enforces it. That is the difference between hoping for safety and being certain of it.
So, onwards and upwards! Let’s start building more cool things you all can get excited about!
Leave a Reply