Multi-Tenancy Rules
CRMBuilder is a multi-tenant platform. Every agency and their clients share the same database, separated by id_company. Getting this wrong is the most common source of data leaks and broken queries.
The golden rule
Every query against a tenant-scoped table must include a WHERE id_company = :id_company clause.
The value comes from the authenticated session JWT — never from user input.
const id_company = session.user.id_company; // always from JWT
Which tables require id_company
Most tables in the CRM are tenant-scoped. The reliable way to check is the Schema Explorer — tables with an id_company column are tenant-scoped and require it on every read and write.
Common scoped tables:
| Table | Notes |
|---|---|
contacts |
Always scope — largest table in the DB |
deals |
Scope via id_company |
companies |
The company records table (not to be confused with the tenant) |
campaigns_items |
Uses customer_id not id_customer — common gotcha |
automations |
Scope or automations fire across tenants |
automation_actions |
Scoped via parent automation |
smslog |
Scope — and note: text column is a BLOB, requires CONVERT(text, CHAR) for string operations |
pipeline_stages |
Scope by id_company |
pb_users |
Tenant's team members |
Tables that are NOT tenant-scoped
A small number of tables are truly global and must not be filtered by id_company:
| Table | Notes |
|---|---|
pb_settings |
Global settings table — has no id_company column |
Attempting to query pb_settings WHERE id_company = X will throw an error. Check the Schema Explorer before adding scoping to any unfamiliar table.
Auth identifier gotcha
pb_users uses login as the authentication identifier — not email.
-- Correct
SELECT * FROM pb_users WHERE login = :login AND id_company = :id_company;
-- Wrong — email column may not exist or may not be the auth field
SELECT * FROM pb_users WHERE email = :email;
The JWT issued on login encodes login, id, and id_company. Always destructure from the token — never re-query for these values using user-supplied input.
Writing new features
When scaffolding any new table or query:
- Add
id_company INT NOT NULLto every new table that stores tenant data - Add a DB index on
id_companyfor any table that will grow large - On INSERT, pull
id_companyfromsession.user.id_company - On SELECT/UPDATE/DELETE, always include
WHERE id_company = ?as the first condition - Never expose raw IDs in URLs without verifying the record belongs to the session's
id_company
Common mistakes
Missing scope on JOIN
-- Wrong — joins across tenants
SELECT c.*, d.title
FROM contacts c
JOIN deals d ON d.id_contact = c.id;
-- Correct
SELECT c.*, d.title
FROM contacts c
JOIN deals d ON d.id_contact = c.id AND d.id_company = c.id_company
WHERE c.id_company = :id_company;
Trusting URL params for id_company
// Wrong — user can spoof this
const id_company = req.params.id_company;
// Correct — always from the verified session
const id_company = session.user.id_company;
Querying pb_settings with a tenant scope
-- Wrong — pb_settings has no id_company column
SELECT * FROM pb_settings WHERE id_company = 5;
-- Correct
SELECT * FROM pb_settings;