Separation
The boundary the database keeps,
not the code
Every CRM claims to be multi-tenant. The claim is cheap; what matters is which layer is holding the line. If the answer is "our application always adds the right filter", then the guarantee is only as good as the least-rested developer who ever wrote a query.
iKonekt was multi-tenant from its first migration rather than retro-fitted afterwards, which sounds like a boast and is really an admission: retro-fitting it is close to impossible to do convincingly, and everyone who has tried has a story.
Separation lives in the database. Every one of the twenty-six tables carries the tenant it belongs to, and the policy that checks it runs on every read and every write, below the application entirely. A query that forgets the boundary does not return somebody else's customers. It returns nothing.
Row-level security, forced
Not a convention, not a code review rule. The database refuses. Seventy-two policies, written out one at a time rather than generated — because a generated policy is a policy nobody has read.
Composite keys
A child record references its tenant and its parent together, as a pair. A stray identifier from the wrong company does not resolve — it simply is not a valid reference.
No default tenant, ever
A request without a scope does not quietly become a request for tenant zero. It throws before it reaches the database. There is no "unscoped" mode to forget to turn off.
The bug that could not announce itself
While the tenancy work was being done, most of the mistakes were loud. Give two companies the same customer domain and the database objects immediately, with an error code and a stack trace; you fix it before lunch.
One did not object. A table of editable prompt overrides was keyed on the prompt's kind alone — not on kind and tenant. A second company editing their own copy would have quietly overwritten the first company's, through entirely valid SQL, with no error, no warning, and no failing test. It was found by reading every remaining unique index after changing a key, one at a time, on the suspicion that the loud ones had company.
We mention it because it is the honest shape of this work. Separation is not a feature you switch on; it is a habit of reading the boring parts of the schema on the assumption that something in there disagrees with you.
The tests, for the record: 942 of them, run against a database rebuilt from zero on every change. They are not there to prove the design is clever. They are there because the failure mode of this particular design is silent, and silent failures need someone shouting on their behalf.
What separation buys a normal person
- A group can run several companies from one install, and a seller in one will never see a customer of another — not by policy, by construction.
- A partner programme, a set of prompts, a claim on a customer: all scoped per company, so one tenant's configuration cannot become everyone's.
- An erasure request is honoured within a boundary rather than across a shared pile, which is what makes it possible to answer honestly.
- And nobody has to remember any of it. That is the point — a guarantee that depends on being remembered is not a guarantee.
One more line that is not about databases
Every sibling service in the family holds its own named credential. Nothing speaks with a shared one, so no service can accidentally act as another — a small discipline that came directly from having once had a shared token and not enjoying it.
It is the same instinct as the database rule. Make the wrong thing impossible rather than discouraged, and you spend your attention on customers instead of on vigilance.
- 126 tablesEvery one carries its tenant, with the check forced on.
- 272 policiesWritten out individually, so each one has been read by a person.
- 3942 testsAgainst a database rebuilt from zero, on every change.
- 40 default tenantsA request without a scope never reaches the data.
Measured on the live build
“Our application always filters by company.”
Always is doing a lot of work in that sentence.
Move the boundary down into the database and a forgotten WHERE clause returns nothing instead of somebody else's customers. An empty screen is a better failure than an awkward introduction.
For groups running several brands →