Two companies pay for the same product. A manager at the first opens the Monday report and sees the second company's rows: calls, invoices, names, a branch that is not theirs. Support treats it as a filter someone forgot. The next release "fixes" it by hiding a column. A month later the export does the same thing, or a nightly email sends one customer's summary to the other. The product is still one product. The promise to each customer was that their records stay theirs.

If you own that product, you do not need a lecture on database design. You need the places this usually leaks, the question to ask before the next report or integration, and a check you can do with two logins. Speed is a different problem. A report that opens faster and still shows the wrong company is not an improvement. The notes below stay with the boundary. They are not a proposal to replace the product.

1. The report is where the mix becomes visible

Customers rarely see your schema. They see a screen with their name in the corner and someone else's numbers in the table. That screen is usually a report: this month's calls, unpaid invoices, a comparison across branches. The report was built when there was one company, or it was built by someone who could see every company and did not notice the filter was optional.

The dropdown is the usual weak point. It remembers the last company, it has an "all" value a customer can reach, or the link in an email already contains another company's id. The screen was tested as an administrator. Administrators are supposed to see across companies. A customer is not. An administrator's login does not test the promise.

Do this with two real customer logins, or with two test logins that must not see each other. Open the same report as each. Write down the rows. You are not looking for a slow spinner. You are looking for a name, a branch or a total that belongs to the other login. If the report is also slow, time it afterwards, on the same screen. How to treat that wait without replacing the product is a separate note: reports that make people wait. Do not let the wait hide the wrong rows, and do not let a performance project widen the query "while we are in there".

2. The nightly file and the email

The screen can be correct and the night job can still be wrong. A job builds an export or sends a summary. It was written as "open the database and walk the rows". When the product grew a second customer, the job kept opening whichever database it was given, or it kept walking every row because nobody added the customer to the message that starts the job.

On one multi-tenant CRM, each branch had its own database. Migrations and indexes were built for that split, not pasted on later as a speed tweak. Background work had a second rule: running it again must not send a second email or write a second score. And the worker had to refuse a connection that did not match the customer named in the message. A job that opens "the" database and trusts the message will happily write one customer's rows as another's. The queue does not know the difference. The message has to name the customer, and the worker has to decline the connection that disagrees.

You can ask for that without reading the worker. Which job sends mail or builds a file? Which customer is that run allowed to touch? What happens if the job starts twice? If the answer is "it uses the main database", ask what "main" means now that you have more than one customer. The shape of a safe retry, and why a second process does not fix a confused message, is in when work leaves the web request.

3. The person who is allowed to see everyone

Someone has to see across customers. You do, support does, sometimes the agency that implements the product. That access is not the bug. The bug is an account that can switch companies without the screen saying which company it is showing, or a switch that is a bookmark a customer forwards, or a shared login left in a chat. The rows are then "leaked" by a person who thought they were looking at their own company.

Make the current company obvious on the screen. Make the switch a deliberate act that is written down. Do not use one shared password for every implementer who has ever joined a call. When an agency hands the product to a new developer, this account is one of the surprises that should be in the first note, not discovered on the live system. That pack is the agency handover.

4. Search, a new integration, a new column

Search is the other place the mix shows up. A box that looks across every customer, because the index was built on the shared table and the customer was left out of it. A missing index makes search feel broken because it takes too long. A missing boundary makes search feel haunted, because the right name appears for the wrong company. Those are different faults. Fixing the speed without the boundary makes the wrong rows arrive sooner. On the CRM above, the indexes were part of the split between branch databases. They were not a later attempt to make a shared pile faster.

A new integration breaks the boundary in a quieter way. Telephony, messaging, accounting, or a step that scores a call: the outside system sends an identifier, and your product writes a row. If the worker trusts that identifier and writes into whichever database it already had open, the call lands on the wrong branch. The integrations on that product sat inside the same rule as the reports. The message named the branch. The worker used that branch's database. Accounting and messaging did not get a private exception because they were "just a sync".

Before the next report, export, search field or integration, add one line to the request: checked as two customers who must not see each other. If the only place you can run that check is production, do not run it against real customers as the first try. A copy, even a rough one, is the place a wrong query is allowed to show the wrong rows. Patching the live product to "see if the filter holds" is the quiet edit described in why a live site is a bad place to find out.

One database per branch is a shape that has worked, with migrations and indexes built around the split. One database for everyone can also work, if every path is forced to the customer and you have proved it with the two logins. The failure is the path that forgets. Do not take another product's layout as a promise that yours is safe, and do not take it as a reason to rebuild before you have opened the report twice.

5. A short plan that is not a new platform

Today: the two-login check on the report people already trust, and on one export or email. Write down what each login saw. Next: the list of jobs and integrations, each with the customer that run may touch, and what a second run is allowed to do. Later: every new screen starts with the line about two customers. A replacement product is a later conversation, and only if the current one cannot keep that line. Most of the time the current one can. It was shipped without the check.

When to bring in a specialist

Open the two logins yourself. You do not need a developer to see another company's name in a table. Ask for help when you cannot get a second login, when a job's database is a shrug, when an integration has already written rows you cannot attribute to a customer, or when the only proposed fix is a faster query that still does not say which customer it is for.

That is the work I take on a product that already has customers: the boundary first, then the report or the job. Services cover SaaS and backend systems, the reporting around them, and the integrations that write back into them. Selected work keeps each result next to the operation that produced it, including a CRM where each branch had its own database. If you want a second look, message me on LinkedIn. Send the report name and what the two logins showed. Do not send a dump of customer rows.