I want to defend a decision we made on day one that has cost us more in build time than almost any other we have made. The decision is that every customer instance of BWI® is its own deployment. Every customer has their own database. Every customer has their own encryption keys. Every customer has their own isolation boundary. Single-tenancy is not a premium tier. It is the floor.
If you have been around SaaS for more than five years, you know this is not the default position of the industry. The default position is multi-tenant. One application instance serves many customers, separated by a tenant ID on every row, with the application code responsible for enforcing the separation. Multi-tenancy is cheaper to operate. It is faster to scale. It is easier to roll out a new feature to every customer at once. The engineering case for it is real.
The engineering case for single-tenancy is also real, and the reason most of the market does not take it seriously is that the cost of the choice has to be paid before any revenue arrives. The choice is structurally aligned with companies that intend to be in business for a long time and structurally misaligned with companies that intend to flip in four years. We are the first kind of company.
What “single-tenant” actually means here
I want to be specific because the word gets used loosely. There are products in the market that claim “single-tenant deployment” and what they mean is “you get your own application server, but the database is shared.” That is not the floor we are talking about.
On BWI, every customer’s production deployment is its own AWS deployment. Its own VPC. Its own database. Its own encryption keys. Its own service identities. Its own audit log storage. Its own retrieval indexes. Its own deployment pipeline. When we deploy a new version of the platform, we deploy it to each customer’s deployment as a separate operation, on the customer’s schedule, with the customer’s rollback boundaries.
The reason this matters operationally is that a problem in one customer’s deployment is structurally incapable of affecting another customer’s deployment. Bad query that exhausts the database connection pool? Stays in the deployment where it happened. Provider key compromised? Rotated for the affected deployment, not for all of them. Auditor forensic request? Scoped to the one deployment by the topology, not by the application code.
The reason it matters from the customer’s compliance lead’s perspective is that “your data lives in our database” is a sentence I have never had to say. It does not live in our database. It lives in a database we operate on the customer’s behalf, dedicated to the customer, in a deployment topology the customer can read on the architecture diagram and verify in the AWS console.
The reason it matters to me as an operator is that I sleep better.
Bubbles, on top of single-tenancy
Single-tenancy at the deployment level solves the cross-customer isolation problem. It does not solve the within-customer isolation problem, and that problem is the one most platforms ignore until it bites them.
The within-customer problem looks like this. A customer runs compliance across twenty of its own entities: separate divisions, subsidiaries, and locations, each with its own scope, its own evidence, its own findings. An analyst working on entity seven should physically not be able to see entity twelve’s data, even if access controls are misconfigured, even if a developer runs a query that omits the scope check, even if a model retrieval accidentally pulls from the wrong index.
We solve that with the Bubbles model. Every customer-scoped piece of content carries a bubble key. Each entity gets its own bubble. The data access layer scopes every retrieval, every search, every cross-product action, every audit log read against the bubble key. The scope check is not in application code. It is in the layer below the application code. The developer does not need to remember to apply the scope; the shared kernel applies it and rejects queries that fail to declare one.
The standard pattern for multi-tenant isolation is row-level security applied in the application layer. The application reads the user’s context, scopes the query, and executes. The pattern works when the developer remembers to apply it. It does not work when the developer forgets, when middleware is bypassed by a debug endpoint, when a background job runs without a user context, when a model retrieval happens in a service worker that did not inherit the request context.
We did not want a security posture that depended on every developer’s discipline every time. We wanted a posture that worked when a developer was tired, sloppy, or new. So the scope check is in the data access layer. The reject is loud, it writes an audit row, and it surfaces to engineering. The developer who writes a query without a scope check learns about it from a unit test, not from the customer’s audit.
The multi-entity scenario
An analyst is reading entity seven’s evidence vault. The analyst goes to lunch. While at lunch, the same analyst’s account is paged into entity twelve. The analyst returns and clicks into entity twelve.
Three things happen, structurally, that I want to call out.
The application’s request context changes. The analyst is now operating in entity twelve’s bubble. Every UI surface re-renders against entity twelve’s data.
Every backend call scopes to entity twelve. The model retrieval pool indexes against the bubble; the data access layer rejects any query that scopes to entity seven; the cross-product action calls carry the entity twelve bubble; the audit log row carries the entity twelve bubble.
If a developer mistake somewhere up the stack tries to load a row from entity seven into the entity twelve view, the data access layer rejects the load. The rejection writes an audit row. The customer’s compliance lead is not the one who finds out.
The platform does what it should do, in the presence of an error condition the developer did not anticipate. That is the design.
The cost of the choice
I do not want to romanticize the choice. It costs us.
It costs us in deployment overhead. Every new customer is a new production environment with its own provisioning, version tracking, and monitoring. The team operates the platform many times instead of once.
It costs us in feature velocity. A feature shipped to one customer is not automatically shipped to every customer; we roll out per-customer with each customer’s rollback boundary respected.
It costs us in cost-to-serve. The unit economics of single-tenancy are worse than multi-tenancy on the operations side. We have made a deliberate choice that the structural property is worth more than the gross margin advantage.
The customers who care about the choice know why we made it. The customers who do not will eventually run into a deployment of a multi-tenant platform that had a bug. When they do, they will call us.
The choice was correct on day one. It is correct now. It will be correct in five years.
Dave Linderman, INT
[Read the security posture → /security]
[Request Early Access]
