# Kế hoạch triển khai Multi-tenant

Tài liệu được lập dựa trên source hiện tại tại ngày 23/09/2026.

## 1. Mục tiêu sản phẩm

Khách hàng điền form đăng ký trung tâm và domain mong muốn. Hệ thống kiểm tra tên miền con, giữ chỗ, tạo tenant và trả ngay đường dẫn:

```text
https://{slug}.ten-mien-saas.vn
```

Wildcard DNS và wildcard SSL được cấu hình một lần cho `*.ten-mien-saas.vn`, vì vậy không cần tạo DNS riêng cho từng khách hàng. Link được trả ngay sau khi form hợp lệ. Trong thời gian database đang được khởi tạo, link hiển thị trang “Đang thiết lập trung tâm”; khi hoàn tất sẽ chuyển sang đăng nhập.

Domain riêng của khách hàng như `app.trungtama.vn` là giai đoạn sau, vì cần xác minh DNS và cấp SSL riêng.

## 2. Đánh giá source hiện tại

### Điểm có thể tái sử dụng

- Toàn bộ ứng dụng dùng chung một adapter Zend DB qua `GlobalAdapterFeature`.
- Model và service đã tập trung quanh adapter này, nên có thể đổi database theo tenant ở đầu mỗi request.
- Hệ thống đã có migrations cho CRM, học phí, bài tập, tài chính, xếp hạng và thông báo.
- `config_tb` đang lưu thông tin trung tâm; với database riêng, bản ghi `id = 1` tiếp tục dùng được cho từng tenant.
- Phân quyền vai trò và session đăng nhập đã tồn tại.

### Các điểm chưa sẵn sàng cho multi-tenant

- Kết nối database đang cố định vào một database trong `config/autoload/global.php`.
- Host hiện chỉ được dùng để tạo URL trong `public_html/define.php`, chưa được ánh xạ sang tenant.
- Các bảng nghiệp vụ không có `tenant_id`.
- Có hơn 400 vị trí truy vấn/table access trực tiếp. Chuyển ngay sang shared database với `tenant_id` có nguy cơ sót điều kiện tenant.
- Session hiện chỉ lưu người đăng nhập, chưa lưu và đối chiếu tenant.
- Cache key chưa chứa tenant nên có thể đụng cache giữa các domain.
- Upload đang dùng thư mục chung.
- Cron/background job chưa có cơ chế chạy lần lượt theo tenant.
- Khóa ứng dụng và thông tin kết nối database đang đặt trong source; cần chuyển sang biến môi trường trước khi mở đăng ký SaaS công khai.
- Chưa có control database, tenant resolver, provisioning job và trang đăng ký tenant.

## 3. Kiến trúc đề xuất

### Mô hình

```text
                             ┌─────────────────────────┐
Khách truy cập domain ──────▶│ Tenant Resolver         │
                             │ host → tenant context   │
                             └───────────┬─────────────┘
                                         │
                    ┌────────────────────┴───────────────────┐
                    │                                        │
          ┌─────────▼─────────┐                    ┌─────────▼─────────┐
          │ Platform database │                    │ Tenant database   │
          │ tenant/domain/job │                    │ nghiệp vụ CMS     │
          └───────────────────┘                    └───────────────────┘
```

Sử dụng:

- Một codebase dùng chung.
- Một platform database quản lý tenant, domain, gói dịch vụ và provisioning job.
- Một database nghiệp vụ riêng cho mỗi tenant.
- Một thư mục upload riêng cho mỗi tenant.
- Một namespace cache và session riêng cho mỗi tenant.

Đây là phương án phù hợp nhất với source hiện tại vì các truy vấn nghiệp vụ cũ tự động chỉ thấy dữ liệu trong database của tenant đã được chọn.

## 4. Database trung tâm

### `platform_tenant_tb`

Các trường chính:

| Trường | Ý nghĩa |
|---|---|
| `id` | Khóa nội bộ |
| `uuid` | ID công khai, không tuần tự |
| `name` | Tên trung tâm |
| `slug` | Tên miền con duy nhất |
| `database_name` | Database nghiệp vụ đã cấp |
| `owner_name` | Người sở hữu ban đầu |
| `owner_email` | Email quản trị ban đầu |
| `owner_phone` | Số điện thoại quản trị |
| `plan_code` | Gói dịch vụ |
| `status` | `provisioning`, `active`, `suspended`, `failed`, `deleted` |
| `provision_error` | Lỗi gần nhất nếu khởi tạo thất bại |
| `date_created`, `date_updated` | Thời gian quản lý |

Ràng buộc bắt buộc:

- Unique cho `uuid`.
- Unique cho `slug`.
- Unique phù hợp cho email owner nếu sản phẩm không cho một người sở hữu nhiều tenant; nếu cho phép thì không đặt unique email ở bảng này.

### `platform_domain_tb`

| Trường | Ý nghĩa |
|---|---|
| `tenant_id` | Tenant sở hữu domain |
| `hostname` | Host chuẩn hóa, unique toàn hệ thống |
| `domain_type` | `subdomain` hoặc `custom` |
| `is_primary` | Domain chính |
| `verification_status` | Trạng thái xác minh DNS |
| `ssl_status` | Trạng thái chứng chỉ |
| `status` | Hoạt động/ngừng hoạt động |

### `platform_provision_job_tb`

Lưu tiến trình tạo tenant để có thể retry an toàn:

- `tenant_id`
- `job_type`
- `status`
- `attempt_count`
- `locked_at`
- `started_at`
- `finished_at`
- `error_message`

## 5. Tenant Resolver

Resolver phải chạy trước khi Zend khởi tạo adapter nghiệp vụ.

Luồng xử lý:

1. Đọc `HTTP_HOST`, bỏ port và chuẩn hóa lowercase.
2. Chỉ chấp nhận hostname hợp lệ; không dùng trực tiếp Host header để ghép SQL hoặc tên file.
3. Tra `platform_domain_tb` bằng platform adapter riêng.
4. Kiểm tra tenant tồn tại và trạng thái cho phép truy cập.
5. Tạo `TenantContext` gồm `tenant_id`, `uuid`, `slug`, `database_name`, `status` và domain chính.
6. Chỉ chấp nhận `database_name` lấy từ registry và đúng whitelist ký tự.
7. Tạo Zend tenant adapter rồi gán vào `GlobalAdapterFeature`.
8. Đưa `TenantContext` vào ServiceManager để controller, cache, upload và log dùng chung.

Các trường hợp đặc biệt:

- Platform domain phục vụ trang đăng ký không cần tenant adapter.
- Domain không tồn tại trả trang “Không tìm thấy trung tâm”, không fallback sang database mặc định.
- Tenant `provisioning` trả trang tiến trình thiết lập.
- Tenant `suspended` trả trang tạm ngưng, không cho truy cập dữ liệu.

## 6. Form đăng ký tenant

### Trường thông tin MVP

- Tên trung tâm.
- Domain mong muốn, hiển thị dạng `{slug}.ten-mien-saas.vn`.
- Họ tên chủ trung tâm.
- Email.
- Số điện thoại.
- Mật khẩu và xác nhận mật khẩu.
- Đồng ý điều khoản sử dụng.

### Trải nghiệm domain

- Tự gợi ý slug từ tên trung tâm nhưng cho phép sửa.
- Chỉ nhận chữ thường ASCII, số và dấu gạch ngang.
- Độ dài đề xuất 3–40 ký tự.
- Không cho bắt đầu/kết thúc bằng dấu gạch ngang hoặc có hai dấu gạch ngang liên tiếp.
- Chặn danh sách từ dành riêng: `www`, `admin`, `api`, `app`, `mail`, `static`, `assets`, `support`, `billing` và các route hệ thống.
- Kiểm tra khả dụng bằng AJAX khi người dùng dừng nhập.
- Unique constraint trong database vẫn là lớp bảo vệ cuối cùng khi hai người đăng ký cùng lúc.

### Phản hồi sau khi đăng ký

Form trả về ngay:

```json
{
  "tenant_id": "uuid",
  "status": "provisioning",
  "url": "https://slug.ten-mien-saas.vn",
  "status_url": "https://ten-mien-saas.vn/setup/uuid"
}
```

Màn hình kết quả có:

- Link tenant và nút sao chép.
- Trạng thái khởi tạo theo thời gian thực hoặc polling.
- Nút “Đi tới trang đăng nhập” khi tenant `active`.
- Thông báo rõ nếu provisioning thất bại và mã hỗ trợ.

## 7. Provisioning tenant

Provisioning nên là job idempotent. Gửi lại cùng job không được tạo trùng dữ liệu.

Thứ tự:

1. Giữ chỗ slug và tạo tenant trạng thái `provisioning` trong transaction.
2. Tạo tên database từ tenant ID, không dùng trực tiếp slug.
3. Tạo database với `utf8mb4`.
4. Chạy schema nền và toàn bộ migration theo thứ tự.
5. Seed role hệ thống.
6. Seed các cấu hình mặc định: CRM stages, CRM sources, loại lớp và danh mục cần thiết.
7. Ghi thông tin trung tâm vào `config_tb` bản ghi `id = 1`.
8. Tạo tài khoản owner với mật khẩu hash mạnh.
9. Ghi `schema_version` cho tenant.
10. Đánh dấu tenant `active`.

Nếu lỗi:

- Rollback phần có thể rollback.
- Giữ tenant ở trạng thái `failed` cùng lỗi kỹ thuật đã làm sạch.
- Cho phép retry từ bước an toàn.
- Không giải phóng slug tự động cho tới khi quản trị quyết định.

## 8. Cô lập dữ liệu ngoài database

### Session

- Session phải chứa `tenant_id` và `member_id`.
- Mỗi request đăng nhập phải đối chiếu tenant trong session với `TenantContext`.
- Không chấp nhận session được tạo từ tenant khác.
- Cookie nên là host-only cho từng subdomain.
- Regenerate session ID sau khi đăng nhập.

### Cache

Mọi cache key cần có prefix:

```text
tenant:{tenant_uuid}:{cache_key}
```

### Upload

Tách file theo tenant:

```text
uploads/tenants/{tenant_uuid}/images/
uploads/tenants/{tenant_uuid}/files/
```

Không nhận tenant ID từ request để tạo đường dẫn; chỉ dùng `TenantContext`.

### Log

Mỗi log nghiệp vụ, lỗi và audit cần chứa `tenant_uuid`, request ID và member ID.

### Background job và cron

Cron trung tâm lấy danh sách tenant `active`, tạo tenant context riêng rồi chạy job từng tenant. Không để adapter của tenant trước bị tái sử dụng cho tenant sau.

## 9. Migration dữ liệu hiện tại

Dữ liệu hiện tại trở thành tenant đầu tiên:

1. Backup database và thư mục upload.
2. Tạo tenant “legacy” trong platform database.
3. Ánh xạ domain hiện tại vào tenant này.
4. Sao chép hoặc đổi tên database hiện tại thành database tenant đầu tiên.
5. Di chuyển upload vào thư mục tenant hoặc tạo lớp tương thích tạm thời.
6. Chạy smoke test toàn bộ trang quan trọng.
7. Chuyển resolver sang chế độ bắt buộc, không còn database fallback.

Không thêm `tenant_id` vào toàn bộ bảng nghiệp vụ trong MVP database-per-tenant.

## 10. Các giai đoạn triển khai

### Giai đoạn 0 — An toàn nền tảng

- Chuyển DB password, API key và secret ra biến môi trường.
- Chuẩn hóa cấu hình development/staging/production.
- Tạo backup và quy trình restore thử nghiệm.
- Xác định domain SaaS chính và bật wildcard DNS/SSL.

**Điều kiện hoàn thành:** ứng dụng hiện tại vẫn chạy bằng cấu hình môi trường, không còn secret cần thiết nằm trong source.

### Giai đoạn 1 — Control plane và TenantContext

- Tạo platform database và ba bảng platform.
- Tạo `PlatformAdapter`, `TenantResolver`, `TenantContext`.
- Thay adapter factory hiện tại bằng tenant-aware factory.
- Thêm trang cho unknown/provisioning/suspended tenant.
- Prefix cache và session theo tenant.

**Điều kiện hoàn thành:** hai hostname test kết nối hai database khác nhau; host không hợp lệ không thể truy cập database mặc định.

### Giai đoạn 2 — Bộ tạo tenant

- Chuẩn hóa schema nền và migration runner.
- Tạo seed mặc định.
- Tạo provisioning service/job với retry và lock.
- Tạo schema version cho từng tenant.
- Chuyển dữ liệu hiện tại thành tenant đầu tiên.

**Điều kiện hoàn thành:** chạy một lệnh có thể tạo tenant mới hoàn chỉnh và đăng nhập được.

### Giai đoạn 3 — Form đăng ký và link tức thời

- Xây trang đăng ký trên platform domain.
- Kiểm tra slug theo thời gian thực.
- Reserve slug bằng transaction và unique constraint.
- Trả URL ngay sau khi form hợp lệ.
- Xây trang tiến trình khởi tạo.
- Tạo owner và gửi email hướng dẫn nếu bật email.

**Điều kiện hoàn thành:** khách điền form, nhận link ngay, tenant chuyển từ `provisioning` sang `active` và đăng nhập được.

### Giai đoạn 4 — Cô lập file, job và tích hợp

- Tách uploads, cache, log theo tenant.
- Sửa cron chạy theo tenant.
- Tách cấu hình Zalo/email/thông báo theo tenant.
- Kiểm tra export/download không truy cập file tenant khác.

**Điều kiện hoàn thành:** kiểm thử chéo tenant không đọc được dữ liệu, file, cache hoặc thông báo của nhau.

### Giai đoạn 5 — Vận hành SaaS

- Trang quản trị platform: tenant, trạng thái, dung lượng, phiên bản schema.
- Suspend/reactivate tenant.
- Migration runner chạy theo batch và có resume.
- Theo dõi lỗi provisioning, database, storage và cron.
- Backup/restore theo tenant.
- Sau MVP mới thêm custom domain, billing và giới hạn theo gói.

## 11. Test bắt buộc

### Cô lập tenant

- Tenant A không thấy học viên, lớp, học phí, CRM, tài chính hoặc file của tenant B.
- ID bản ghi giống nhau ở hai database không gây truy cập chéo.
- Session đăng nhập ở domain A không dùng được ở domain B.
- Cache và thông báo của hai tenant không trộn nhau.

### Provisioning

- Hai request đồng thời cùng slug chỉ một request thành công.
- Retry job không tạo trùng owner, role hoặc seed.
- Lỗi migration giữ trạng thái `failed` và có thể retry.
- Tenant chưa active không truy cập được trang quản lý.

### Domain

- Host giả, host có port, chữ hoa và hostname không tồn tại được xử lý đúng.
- Reserved slug bị từ chối.
- Wildcard subdomain dùng HTTPS hợp lệ.

### Nâng cấp ứng dụng

- Migration chạy được cho tenant mới và tenant cũ.
- Có báo cáo tenant nào chưa lên schema mới.
- Có thể rollback code mà không làm mất dữ liệu tenant.

## 12. Thứ tự ưu tiên đề xuất

Không bắt đầu bằng form đăng ký. Form chỉ nên làm sau khi resolver, database template và provisioning service đã chạy ổn bằng lệnh nội bộ.

Thứ tự an toàn:

1. Tách secrets và chuẩn hóa môi trường.
2. Platform database.
3. Tenant resolver và dynamic adapter.
4. Migration tenant hiện tại.
5. Provisioning service chạy nội bộ.
6. Test cô lập hai tenant.
7. Form đăng ký và trang trả link.
8. Upload/cache/cron/integration hardening.
9. Platform admin, billing và custom domain.

## 13. Quyết định cần chốt trước khi code

- Domain SaaS chính dùng cho wildcard subdomain.
- Tenant có được dùng thử tự động hay cần quản trị duyệt.
- Một email có được sở hữu nhiều trung tâm hay không.
- Gói mặc định và giới hạn ban đầu.
- Thời gian giữ slug khi provisioning thất bại.
- Có yêu cầu email verification trước khi đăng nhập hay không.
- Hạ tầng MySQL hiện tại có quyền tạo database tự động hay cần database pool được tạo sẵn.
