Perfect — here is a clean, final, implementation-ready checklist for introducing RSP (Reseller / Service Provider) support using a new rsp_id field, without breaking your existing system.

This is written so you can execute it step by step, safely.

✅ RSP IMPLEMENTATION – CLEAN STEPS (FINAL)

This approach is:

Backwards-compatible

Auditable

Role-clean

Minimal schema change

Easy to reason about long-term

0️⃣ FINAL ROLE MODEL (LOCK THIS FIRST)
super_admin        – Global
platform_admin     – Global
support             – Global
rsp_admin           – Own customer base only
company_admin       – Own company only
manager             – Own company only
billing             – Own company only
user                – Own company only
viewer              – Own company only


⚠️ admin remains invalid
⚠️ Backend enforcement only

1️⃣ DATABASE CHANGE (SINGLE COLUMN)
Add rsp_id to companies
ALTER TABLE companies
ADD COLUMN rsp_id BIGINT UNSIGNED NULL
AFTER id;

Meaning (IMPORTANT)

rsp_id = NULL → Direct platform customer or an RSP itself

rsp_id = <company.id> → Customer of that RSP

No other table needs changes.

2️⃣ RSP REPRESENTATION (NO NEW TABLE)

RSPs are companies.

An RSP is a row in companies

RSP company has rsp_id = NULL

Customers of the RSP have rsp_id = <rsp_company_id>

Example
id	company_name	rsp_id
1	Platform Direct Co	NULL
2	RSP Alpha	NULL
3	Alpha Client A	2
4	Alpha Client B	2
3️⃣ USERS & ROLES (NO SCHEMA CHANGE)
Users table stays as-is
users.company_id → company they belong to
users.role       → role_key

RSP Admin rule

A user with:

role = rsp_admin
company_id = <rsp_company_id>


Controls all companies where:

companies.rsp_id = users.company_id

4️⃣ BACKEND SCOPE RESOLUTION (CANONICAL)

Create one helper and use it everywhere.

function resolveCompanyScope(array $user): array|string {

    switch ($user['role']) {

        case 'super_admin':
        case 'platform_admin':
        case 'support':
            return 'GLOBAL';

        case 'rsp_admin':
            return [
                'rsp_id' => $user['company_id']
            ];

        default:
            return [
                'company_id' => $user['company_id']
            ];
    }
}


This becomes the single source of truth.

5️⃣ APPLY SCOPE IN QUERIES (PATTERN)
Companies list
$scope = resolveCompanyScope($_SESSION);

if ($scope === 'GLOBAL') {
    $sql = "SELECT * FROM companies";
    $params = [];
} elseif (isset($scope['rsp_id'])) {
    $sql = "SELECT * FROM companies WHERE rsp_id = :rsp_id";
    $params = [':rsp_id' => $scope['rsp_id']];
} else {
    $sql = "SELECT * FROM companies WHERE id = :company_id";
    $params = [':company_id' => $scope['company_id']];
}

Users list
-- GLOBAL
SELECT u.* FROM users u;

-- RSP ADMIN
SELECT u.*
FROM users u
JOIN companies c ON u.company_id = c.id
WHERE c.rsp_id = :rsp_id;

-- COMPANY USER
SELECT * FROM users WHERE company_id = :company_id;

6️⃣ COMPANY CREATION FLOW (UPDATED)
When creating a company:

Global admin → can choose rsp_id or leave NULL

RSP admin → rsp_id is forced to their own company

Company admin → cannot create companies

Enforce in backend:
if ($user['role'] === 'rsp_admin') {
    $rspId = $user['company_id'];
}


Frontend choice is cosmetic only.

7️⃣ MODULES, SUBSCRIPTIONS, BILLING

✅ No changes required

Why:

Subscriptions are company-based

Billing remains company-based

RSP is only a visibility boundary, not a billing entity (for now)

You can add revenue splits later without touching RBAC.

8️⃣ AUDIT LOGS (ALREADY COMPATIBLE)

Audit entries already record:

actor_user_id

company_id

This is sufficient to:

Attribute RSP actions

Prove tenant isolation

Investigate disputes

No schema change required.

9️⃣ FRONTEND (OPTIONAL, NOT AUTHORITATIVE)

Frontend may:

Hide companies outside scope

Hide RSP-only features

Show “My Customers” vs “My Company”

⚠️ Backend remains authoritative.

🔒 SECURITY GUARANTEES THIS MODEL PROVIDES

✔ RSPs cannot see each other
✔ Customers cannot escape their RSP
✔ Global roles retain oversight
✔ Clean audit trails
✔ No role explosion
✔ No user-ownership hacks

🧠 WHAT NOT TO DO (IMPORTANT)

❌ Do not overload owner_user_id
❌ Do not infer hierarchy from users
❌ Do not duplicate RSP logic per endpoint
❌ Do not add per-RSP tables

✅ IMPLEMENTATION ORDER (SAFE SEQUENCE)

Add rsp_id column

Add rsp_admin role

Add scope resolver helper

Update company & user list endpoints

Update company creation logic

(Optional) Update frontend labels

You can stop after step 4 and already be functional.

🟢 FINAL VERDICT

This is the correct, final, production-grade way to support:

Multiple service providers

Isolated customer bases

Clean RBAC

Future growth

Once implemented, do not revisit this model — it scales cleanly.

If you want next, I can:

Write the exact SQL seed data

Patch your existing endpoints one by one

Add RSP dashboards

Add revenue split logic (later)

Just tell me where you want to go next.