# Kaabe POS – Phase 2: Architecture Design

**Based on:** the Phase 1 audit of the actual PHP POS 19.1 source (CodeIgniter 3.1.13, 165-table schema), the frozen Super Admin prototype v1.0, the Kaabe central database design (step 2), and the four confirmed decisions:

- **First tenant:** `ssgplatforms_deeq`.
- **Five extra plan modules:** Appointments, Work Orders, Deliveries, Messages, Price Rules.
- **Hosting:** InMotion Hosting.
- **Subscription payments:** manual for now, gateway-ready.

**Status:** design for approval. No implementation code yet. Items marked **(verify)** need confirming on the real server.

---

## 1. System architecture

### 1.1 Components

| # | Component | Technology | Runs at | Owns |
|---|---|---|---|---|
| A | **Kaabe Platform** (Super Admin, billing, provisioning, sync, internal API) | Laravel 12, PHP 8.3 | `admin.<domain>` | Central database `<cp>_kaabe` |
| B | **Kaabe POS** (the existing PHP POS, patched) | CodeIgniter 3.1.13 / PHP POS 19.1, PHP 8.3 (fallback 8.2) | `<tenant>.<domain>` (one codebase, many subdomains) | One database per business |
| C | **Tenant registry** | Generated PHP file (opcache-cached) | Shared folder outside web root | Subdomain → database connection, status |
| D | **Scheduler and queue** | cPanel cron → `artisan schedule:run` → database queue | Server | Background jobs |
| E | **Backups** | mysqldump + InMotion backups + off-server copy | Server + remote storage | Recovery |

`<domain>` is your chosen product domain (for example `kaabepos.com`). `<cp>` is the cPanel account prefix (today: `ssgplatforms`).

```mermaid
flowchart TB
    subgraph Users
      SA[Super Admin staff]
      BU[Business users<br/>owner · admin · manager · cashier]
    end
    subgraph Server["InMotion VPS (cPanel/WHM)"]
      P["Kaabe Platform<br/>Laravel · admin.domain"]
      POS["Kaabe POS<br/>PHP POS 19.1 patched · *.domain"]
      REG[("tenants.php<br/>registry")]
      CDB[("Central DB<br/>cp_kaabe")]
      T1[("cp_deeq<br/>tenant 1")]
      T2[("cp_kb0002<br/>tenant 2")]
      TN[("cp_kbNNNN<br/>tenant N")]
      CRON[cron → scheduler → queue]
    end
    SA --> P
    BU --> POS
    P --> CDB
    P -- writes --> REG
    POS -- reads --> REG
    POS --> T1 & T2 & TN
    P -- provision · entitlements · sync --> T1 & T2 & TN
    POS -- signed internal API --> P
    CRON --> P
    CRON -- per-tenant POS jobs --> POS
```

### 1.2 Why two deployables make one product

- **Business users only ever see Kaabe POS.** It is the existing PHP POS with Kaabe branding, a Kaabe business dashboard, and an "Account & Subscription" page. They never visit the platform.
- **Staff only ever see the Super Admin.**
- **The two are joined by data, not duplicated screens.**
  - The platform creates and controls each tenant database.
  - The POS reads its plan limits from its own database.
  - The platform reads sales totals back from each tenant database.
- **Keeping the POS as a separate deployable is what preserves it.** Its 41 controllers, 188 models, 118 reports and checkout keep working as they are, and future PHP POS updates can still be merged. Rewriting it inside Laravel would throw that away.

### 1.3 Business dashboard placement

The business dashboard lives **inside the POS**, because every number it shows lives in the tenant database:

- Today's and monthly sales
- Transactions
- Products and inventory value
- Low stock
- Customers, suppliers and expenses
- Profit, from the existing P&L report models
- Branch performance
- Recent transactions

It is built as a new `Kaabe_dashboard` controller that reuses existing models (`Sale`, `Item`, `Inventory`, `reports/Summary_profit_and_loss`, `reports/Inventory_low`). It becomes the post-login home, replacing `Home::index`, which stays reachable.

---

## 2. Multi-tenant architecture

### 2.1 Model: database per tenant

One MySQL/MariaDB database per business, with its own database user that has privileges **only on that database**. This is the model PHP POS's own cloud uses (`Cron::get_accounts()`), so the POS code needs no `tenant_id` in its queries.

| Layer | How isolation is enforced |
|---|---|
| Database | Each tenant's database user can access only its own database (cPanel `set_privileges_on_database`). A request for tenant A physically cannot query tenant B. |
| Request routing | A `pre_system` hook resolves the tenant from the `Host` header **before** any controller runs. An unknown host gets a 404; a suspended or closed tenant gets a branded blocked page. |
| Session | Sessions live in each tenant's own `phppos_sessions` table. Cookies are host-only (no `Domain=` attribute), `Secure`, `HttpOnly`, `SameSite=Lax`. |
| API | API keys live in each tenant's `phppos_keys`, so a key from A is unknown in B's database. |
| Files | Uploads are stored in the tenant database (`phppos_app_files`), so they are isolated with it. |
| Background jobs | Each POS cron run is started with `KAABE_TENANT=<id>` and connects only to that tenant's database. |
| Platform | The platform opens tenant connections by `tenant_id` only, through `TenantConnectionManager`. Staff queries across tenants go through central summary tables, never through ad-hoc joins. |

### 2.2 Tenant resolution (POS side)

```
Request: https://deeq.kaabepos.com/sales
   │
   ├─ index.php → pre_system hook  kaabe_resolve_tenant()
   │     host = "deeq.kaabepos.com" → subdomain "deeq"
   │     $reg = require KAABE_SHARED.'/tenants.php'   (opcache-cached array)
   │     $t = $reg['deeq'] ?? 404
   │     if ($t['status'] in [suspended, closed]) → blocked page (HTTP 403)
   │     define('KAABE_TENANT_ID', $t['id']);  $GLOBALS['kaabe_db'] = $t['db'];
   │
   └─ config/database.php  →  $db['default'] = array_merge(defaults, $GLOBALS['kaabe_db'])
```

- **How the registry is written.** The platform rewrites it atomically (write to a temp file, then `rename`) whenever a tenant is created, suspended, reactivated or closed, or has its database credentials rotated. Database passwords in the file are encrypted with a registry key held only in the POS environment, and decrypted in the hook.
- **Why a file, not a central database call.** Per-request lookups then cost nothing, and a platform outage does not take down POS checkout.
- **Command-line use.** When there is no `Host` header, `KAABE_TENANT` selects the tenant for cron.

### 2.3 Scaling path

| Stage | Tenants | Layout |
|---|---|---|
| Launch | 1–100 | Single VPS: web + MariaDB |
| Growth | 100–500 | Larger VPS or dedicated server; tune the InnoDB buffer pool; move backups off-box |
| Scale | 500+ | Separate database server(s). `pos_instances.db_host` is already per tenant, so tenants can be spread across database servers. Redis for cache and queue. |

The architecture does not change between stages; only where tenant databases live does.

---

## 3. Database integration strategy

### 3.1 Single owner for every kind of data

| Data | Master | Other side keeps |
|---|---|---|
| Business, status, program, owner contact | Central `tenants` | Status copy in the tenant's `app_config.kaabe_status` |
| Plan, limits, modules | Central (`plans`, `plan_limits`, `plan_module`, overrides) | Computed entitlements in the tenant's `app_config.kaabe_entitlements` |
| Subscriptions, invoices, payments | Central | — (the POS reads them through the internal API) |
| Branches and warehouses | **Created through the platform**; operational record is the tenant's `phppos_locations` | Central `branches.pos_location_id` (1:1 link) |
| Business users, roles, permissions | Tenant (`phppos_employees`, `permission_templates`, `permissions*`) | Central `tenant_usage.users_count` only |
| Products, stock, sales, customers, suppliers, purchases, expenses, invoices to customers | Tenant | Central daily totals (`tenant_daily_metrics`), counts (`tenant_usage`), live feed (`sale_events`) |
| Platform staff | Central `users` | — |
| Platform settings | Central `settings` | — |
| POS settings | Tenant `phppos_app_config` | — |

There are no foreign keys across databases. Links are IDs (`pos_location_id`, `pos_sale_id`), checked by the sync job.

### 3.2 Additions to tenant databases

These are **additive only**: no existing table is altered or dropped.

| Object | Purpose |
|---|---|
| `phppos_kaabe_location_meta (location_id PK, kind ENUM('branch','warehouse'), central_branch_id)` | Warehouses, and the link to central |
| `phppos_kaabe_meta (key PK, value)` | Kaabe schema version and tenant ID, as a safety check that the database matches the registry |
| `app_config` keys `kaabe_status`, `kaabe_entitlements` (JSON), `kaabe_entitlements_version` | Pushed by the platform |
| Index `phppos_sales(sale_time, location_id)` **(verify it doesn't already exist)** | Keeps the sync queries cheap |

These are created by Kaabe migration files named `kaabe_*`, run by the platform's tenant migrator. They are kept out of the vendor migration sequence so vendor updates can't collide with them.

### 3.3 Central database changes from step 2

| Change | Reason |
|---|---|
| Replace `plans.max_*` columns with `limit_definitions` + `plan_limits` (key/value) | New limits can be added without schema changes |
| Add `tenant_entitlement_overrides` | Grant one tenant an exception, such as an extra branch, without a custom plan |
| `modules` gains `pos_modules` (JSON), `pos_actions` (JSON), `category`, `is_beta` | Maps a plan module to the real POS modules and actions (§5) |
| Add `subscription_events` | Full manual subscription history (§7) |
| `payments` gains `status`, `gateway`, `gateway_reference`, `gateway_payload`, `received_by`, `notes`; add `gateway_events` | Ready for a gateway later (§7) |
| `pos_instances` gains `cpanel_db_name`, `cpanel_db_user`, `registry_version`, `entitlements_pushed_at`, `sync_cursor` (JSON), `hmac_secret` (encrypted) | cPanel naming, push and sync tracking, signed internal API |
| Add `notifications` + `notification_preferences` | Platform notifications (email, in-app; SMS later) |
| Add `system_events` | System log: job failures, provisioning errors, sync errors |
| `tenant_usage` gains `branches_count`, `warehouses_count`, `db_size_mb` | Plan enforcement and storage |

---

## 4. Tenant / business structure

```
Kaabe Platform
 └── Tenant (business)          central: tenants            tenant DB: whole database
      ├── Subscription → Plan   central                     pushed: app_config.kaabe_entitlements
      ├── Branch                central: branches(kind=branch)    ⇄ phppos_locations + kaabe_location_meta
      │    ├── Registers                                          phppos_registers (location_id)
      │    └── Users assigned                                     phppos_employees_locations
      ├── Warehouse             central: branches(kind=warehouse) ⇄ phppos_locations (no registers)
      └── Users                 tenant: phppos_employees + people
           └── Role                  tenant: permission template (+ per-location permissions)
                └── POS data         tenant: items, location_items, sales, receivings, customers …
```

**Branch lifecycle.** A branch is created in the Super Admin, or requested by the business owner in the POS. In both cases the **platform** checks the plan limit, then creates the location through the POS API (`Locations` endpoint, so the POS's own defaults, registers and tax settings apply). It then writes the meta row and the central row.

**Warehouses.**
- A warehouse is a location with `kind = warehouse`.
- Kaabe patches hide it from register selection and from "choose location to sell", and no register is created for it.
- Stock transfers use the POS's existing `receivings.transfer_to_location_id`.

**Owner.** The first employee created at provisioning is the owner. It is recorded in `kaabe_meta.owner_person_id`, and a guard stops it being deleted or deactivated from within the POS.

---

## 5. SaaS plan and module architecture

### 5.1 Module registry (20 modules)

| Key | Name | POS modules / actions it unlocks | Core |
|---|---|---|---|
| `pos` | POS / Sales | `sales` | ✔ |
| `products` | Products & categories | `items`, `item_kits` | ✔ |
| `inventory` | Inventory | `items` inventory actions, inventory counts | ✔ |
| `customers` | Customers | `customers` | ✔ |
| `reports_basic` | Standard reports | `reports` (standard set) | ✔ |
| `purchases` | Purchases | `receivings` | |
| `suppliers` | Suppliers | `suppliers` | |
| `expenses` | Expenses | `expenses` | |
| `invoices` | Invoices & payments | `invoices` | |
| `employees` | Employees | `employees` (managing staff beyond the owner) | |
| `multi_branch` | Multi-branch | `locations` (count set by limit) | |
| `warehouses` | Warehouses | Kaabe warehouse kind + transfers | |
| `reports_adv` | Advanced analytics | P&L, commissions, price variance and similar report groups | |
| `loyalty` | Loyalty & gift cards | `giftcards`, loyalty settings | |
| `api` | API access | Business-created API keys | |
| **`appointments`** | Appointments | `appointments` | |
| **`work_orders`** | Work orders | `work_orders` | |
| **`deliveries`** | Deliveries | `deliveries` | |
| **`messages`** | Messages | `messages` | |
| **`price_rules`** | Price rules | `price_rules` | |

The exact report-to-group mapping is a config array in the patch, derived from the 118 report models.

### 5.2 Adding a module later

Adding a module needs no schema change and no change to the POS core:

1. Insert a `modules` row with its `pos_modules` and `pos_actions` mapping (Super Admin screen or seeder).
2. Tick it on the plans that include it.
3. The next entitlement push carries it. The POS wrapper enforces any POS module ID listed.

A future Kaabe-only feature (for example an SMS module) uses the same flag. Its own code checks `kaabe_entitled('sms')`.

### 5.3 Limits

`limit_definitions` defines each limit: key, label, unit, how it's measured, and where it's enforced. The seeded limits:

| Key | Measured as | Enforced at (POS) |
|---|---|---|
| `branches` | Active branch-kind locations | Platform branch creation + `Locations` save |
| `warehouses` | Warehouse-kind locations | Same |
| `users` | Active, non-deleted employees | `Employee::save_employee` (new only) |
| `products` | Non-deleted items | `Item::save` (new only), covering the UI, Excel import and API |
| `storage_mb` | Tenant database size (`information_schema`) | `Appfile` save (uploads) |
| `registers` | Registers | `Register` save |
| `api_keys` | Keys | API key creation |

`NULL` means unlimited.

**Effective entitlements** = plan limits and modules + tenant overrides − modules switched off platform-wide. They are computed centrally and pushed as JSON with a version number.

### 5.4 Enforcement layers

Enforcement is on the backend, not just hidden menus:

1. **Entitlement push.** The platform writes `kaabe_entitlements` to the tenant's `app_config` on every plan change, override, module switch or subscription status change, and on a nightly re-push.
2. **POS module gate.** `Employee::has_module_permission` and `has_module_action_permission` are wrapped. If the module isn't entitled they return FALSE, so `Secure_area` already redirects to `no_access`, and menus built from permissions disappear automatically.
3. **POS limit gate.** A `Kaabe_limits` library is called at the start of the save methods above for **new** records. Over the limit, the save returns a validation error ("Your plan allows 1,000 products. Upgrade to add more."). Existing records are never touched.
4. **API gate.** The same checks run inside the API controllers. They call the same models, so no extra work is needed.
5. **Downgrades.**
   - The platform blocks a downgrade that would put the tenant over a limit, unless a staff member explicitly forces it.
   - If forced, the tenant can keep using and editing everything, but cannot create more of the exceeded resource.

---

## 6. Authentication and permissions architecture

### 6.1 Two identity domains

| | Platform staff | Business users |
|---|---|---|
| Stored in | Central `users` | Tenant `phppos_employees` + `people` |
| Login at | `admin.<domain>` | `<tenant>.<domain>` |
| Password hashing | Laravel `hashed` (bcrypt, cost 12) | **Upgraded:** MD5 → `password_hash()` (bcrypt), see §6.3 |
| 2FA | **Required** for Super Admin, Support, Billing (TOTP) | Available through the existing google2fa support; the owner can require it for their business |
| Sessions | Laravel database sessions, `Secure`/`HttpOnly`/`SameSite=Lax`, 30 min idle timeout, ID regenerated on login | CI3 database sessions with the hardening from audit S4 |
| Lockout | 5 failed logins → 15 min lock per account+IP | Same, through the existing `login_failed_time_period` + a Kaabe counter |
| CSRF | Laravel CSRF | CI3 CSRF enabled (audit S3) with a global AJAX token |

### 6.2 Roles

**Platform roles (central, configurable).**
- The system roles are Super Admin, Operations Manager, Support Agent, Billing Manager and Auditor, with 25+ permissions (already seeded in step 2).
- Custom roles can be created in the Super Admin.
- Every controller action is guarded by a Laravel Policy or `can:` middleware. There are no front-end-only checks.

**Business roles (tenant, configurable).**
- These use PHP POS **permission templates**, which already support modules, actions and per-location scope.
- The provisioner seeds five templates into each new tenant (and into tenant #1 without changing existing employees):

| Template | Scope |
|---|---|
| Business Owner | All modules and actions, all locations (non-removable) |
| Business Admin | All except Kaabe account and subscription |
| Branch Manager | Sales, items, inventory, receivings, customers, expenses, reports – **limited to assigned locations** |
| Cashier | Sales, customers (search/add), own register – assigned locations |
| Inventory Manager | Items, inventory, receivings, suppliers, transfers, inventory reports |

Owners and admins can create more templates or adjust these in the POS's existing Employees screen, and permissions are enforced by the POS's existing `Secure_area` checks. Kaabe's module gate (§5.4) sits **on top**: a role can never grant a module the plan doesn't include.

### 6.3 Password migration (MD5 → bcrypt)

Existing users migrate silently, without a mass reset:

- **Existing users are upgraded on their next login.** The patched `Employee::login()`:
  1. If the stored hash starts with `$2y$`, verify with `password_verify`.
  2. Otherwise compare `md5($input)` with `hash_equals`. On a match, immediately store `password_hash($input)`.
  3. Every other save path (`save_employee`, password change, reset) writes `password_hash()`.
- **Accounts that never log in.** After 90 days, remaining MD5 hashes are flagged and those users are forced to reset.

### 6.4 Staff access to a business (support login)

Super Admin → business panel → "Open POS as support":

1. The action requires the `tenants.support_login` permission and a written reason. It is recorded in the central audit log.
2. The platform creates a single-use token (HMAC-SHA256 with the tenant's `hmac_secret`, 60-second expiry, nonce).
3. The browser goes to `https://<tenant>.<domain>/kaabe_sso?token=…`. The POS verifies the token and logs in as a hidden `kaabe_support` employee: non-deletable, no password login, all permissions, and recorded in the POS audit.
4. A banner shows "Kaabe support session" throughout. Owners can turn support access off in their Account page.

### 6.5 Staff management of business users

Staff can do this from the Super Admin, without logging in as support:

- **List users.** Reads `phppos_employees` for the tenant directly (read-only).
- **Deactivate, reactivate, reset password.** Goes through the POS API `Employees` endpoint, so POS business logic and audit apply.
- **Change a user's role.** Assigns a permission template through the API.

---

## 7. Subscription architecture (manual now, gateway-ready)

### 7.1 Data model

```mermaid
erDiagram
    tenants ||--o{ subscriptions : has
    plans ||--o{ subscriptions : for
    subscriptions ||--o{ subscription_events : history
    subscriptions ||--o{ invoices : bills
    invoices ||--o{ payments : settled_by
    payments }o--o| gateway_events : "later: webhook"
```

| Table | Key fields |
|---|---|
| `subscriptions` | tenant, plan, `billing_cycle`, `status`, `price` (snapshot), `currency`, `starts_at`, `current_period_end`, `trial_ends_at`, `grace_ends_at`, `auto_suspend` (bool), `cancelled_at` |
| `subscription_events` | subscription, `type` (created, activated, extended, plan_changed, suspended, resumed, cancelled, expired, payment_recorded), `from`/`to` JSON, `actor_id`, `reason`, `occurred_at` |
| `invoices` | number, tenant, subscription, period, amounts, `status` (draft, pending, paid, partially_paid, overdue, void), `issued_at`, `due_at`, `paid_at` |
| `payments` | invoice, tenant, `amount`, `currency`, `method` (cash, zaad, edahab, bank, card, gateway), `status` (pending, succeeded, failed, refunded), `paid_at`, `reference`, `notes`, `received_by`, `gateway` (NULL = manual), `gateway_reference`, `gateway_payload` |
| `gateway_events` | `gateway`, `event_id` (unique, for idempotency), `payload`, `processed_at` – empty until a gateway is added |

### 7.2 Manual operations

Each operation is available to users with `subscriptions.manage`. Every one writes a `subscription_events` row and an audit log entry.

| Operation | Effect |
|---|---|
| Create subscription | Choose plan, cycle, start date, price (defaults to the plan price), trial (optional). Previous subscription ends. Entitlements are pushed. |
| Activate | trial/past_due/suspended → active; the tenant is unblocked if it was blocked for billing |
| Extend | Moves `current_period_end` by N days or months (optionally issuing an invoice) |
| Change plan | Checks limits (§5.4); immediate or at period end; price snapshot updated; entitlements pushed |
| Suspend subscription | subscription → suspended **and** tenant → suspended (POS login blocked, data kept), with a reason |
| Mark payment status | On an invoice: pending / paid / partially paid / void |
| Record payment | Amount, date, method, reference, notes → `payments` row; invoice status recalculated |
| View history | Timeline from `subscription_events` + invoices + payments |

### 7.3 Automation (scheduler, daily)

- **Invoices due.** An invoice is issued N days before `current_period_end`; the setting is on by default and can be turned off.
- **Overdue.** Unpaid past `due_at` → overdue; subscription → past_due; the business sees a warning banner in the POS.
- **Grace period.** After `grace_ends_at`: if `auto_suspend` is on, suspend; otherwise a "needs attention" alert goes to staff. **Default: off**, so suspension stays a manual decision, matching your manual process.

### 7.4 Adding a gateway later

- **Interface.** A `PaymentGateway` interface (`createCharge`, `verifyWebhook`, `parseEvent`) with a `ManualGateway` implementation today.
- **What a real gateway adds.** A class, a webhook route `/webhooks/{gateway}`, and rows in `gateway_events`.
- **What doesn't change.** Invoices, payments and subscription logic stay as they are, because a webhook simply records a `payment` like a staff member does.

---

## 8. API / integration architecture

### 8.1 Four channels

| # | Direction | Channel | Used for | Auth |
|---|---|---|---|---|
| 1 | Platform → tenant DB | Direct MySQL connection, one per tenant | Provisioning, entitlement push (`app_config`), sync reads, Kaabe tenant migrations | Tenant's own database user (encrypted in `pos_instances`) |
| 2 | Platform → POS | Existing REST API `api/v1` | Writes that need POS business logic: create locations, employees, role assignment | Per-tenant **platform API key** (`phppos_keys`, SHA-1 stored, IP-restricted to the server) |
| 3 | POS → Platform | New **internal API** `admin.<domain>/api/internal/v1` | Business "Account & Subscription" page (plan, usage, invoices, payments), support tickets from the POS, branch requests, entitlement refresh | HMAC-SHA256 signature (tenant secret) + timestamp (±5 min) + nonce |
| 4 | Staff tools → Platform | Platform REST `api/v1` (later) | Future mobile admin and integrations | Laravel Sanctum tokens, staff only |

### 8.2 Conventions

| Topic | Rule |
|---|---|
| Versioning | URL prefix `/v1`; breaking changes → `/v2` |
| Validation | Laravel Form Requests; POS side uses CI3 `form_validation` |
| Errors | `{ "error": { "code": "limit_exceeded", "message": "…", "details": {…} }, "request_id": "…" }` with correct HTTP codes (400/401/403/404/409/422/429/500) |
| Rate limiting | Internal API: 120 req/min per tenant; staff API: 60 req/min per token; login: 5/min per IP+account |
| Idempotency | `Idempotency-Key` header on provisioning and payment endpoints |
| Logging | Request ID on every call; failures written to `system_events`; secrets never logged |
| Timeouts | Platform → POS calls: 10 s connect, 30 s total, 2 retries with backoff for GETs only |

### 8.3 Sync (tenant → central totals)

Queued jobs, per tenant, spread over time so they don't all hit the database at once:

| Job | Frequency | Reads (tenant) | Writes (central) |
|---|---|---|---|
| `SyncSales` | Every 5 min | `phppos_sales` where `sale_time >= today-1` and not deleted or suspended, grouped by `location_id`, day | Upsert `tenant_daily_metrics` for today and yesterday (catches late edits and deletions) |
| `SyncSaleEvents` | Every 5 min | New sales since `sync_cursor.last_sale_id` (max 50) | `sale_events` |
| `SyncUsage` | Hourly | Counts of employees, items, locations, `location_items where quantity <= reorder_level`; database size | `tenant_usage` |
| `BackfillMetrics` | Once per tenant | Full sales history | `tenant_daily_metrics` |
| `HealthCheck` | Every 15 min | `SELECT 1`, registry match, POS `/index.php/login` HTTP 200 | `pos_instances.health_status` |

Rough cost: about 5 small grouped queries per tenant every 5 minutes. At 1,000 tenants that is about 17 queries per second spread over time, well within one MariaDB server.

---

## 9. InMotion Hosting deployment architecture

### 9.1 Plan type: **VPS (cPanel/WHM, root access) recommended**

| Need | Shared hosting | VPS / Dedicated |
|---|---|---|
| Tenant subdomains with SSL | AutoSSL per subdomain only | Per-subdomain AutoSSL **or** a wildcard certificate (InMotion installs wildcards only on VPS or Dedicated) |
| PHP version per app | MultiPHP (server-dependent) | Full control through EasyApache/MultiPHP |
| Cron every minute, long jobs | Limited by shared resource rules **(verify)** | Yes |
| Databases created by API | cPanel UAPI | cPanel UAPI (+ WHM if needed) |
| MariaDB tuning, backups, firewall | No | Yes (root) |
| Suitable for | Pilot with a few tenants | Production |

**Recommendation:** start production on an InMotion **managed cPanel VPS** (at least 4 vCPU / 8 GB RAM / 160 GB SSD, **to be sized with InMotion against expected tenants**), and move to Dedicated past a few hundred tenants. If you are currently on shared hosting (the `ssgplatforms_` prefix suggests a cPanel account), migrating to a VPS is step 0 of Phase 6.

### 9.2 Server requirements

| Item | Requirement |
|---|---|
| OS / panel | AlmaLinux 8/9 with cPanel & WHM (InMotion standard) |
| Web server | Apache (EasyApache 4) + PHP-FPM; `mod_rewrite`, `mod_deflate`, `mod_headers`, `mod_security` |
| PHP | **8.3** for both apps (Laravel 12 needs 8.2+; PHP 8.1 is past end of life). The POS on 8.3 is **(verify)** in Phase 5, with 8.2 as fallback through MultiPHP per domain. |
| PHP extensions | mysqli, pdo_mysql, openssl, curl, mbstring, gd **or** imagick, bcmath, gmp, soap, zip, xml, simplexml, dom, intl, fileinfo, sodium, ctype, tokenizer, opcache |
| PHP settings (production) | `memory_limit=512M`, `max_execution_time=120` (web) / 0 (CLI), `upload_max_filesize=20M`, `post_max_size=25M`, `display_errors=Off`, `expose_php=Off`, `session.cookie_secure=1`, `session.cookie_httponly=1`, `opcache.enable=1` |
| Database | **MariaDB 10.6+** (10.11 LTS preferred). `innodb_buffer_pool_size` ≈ 50–60% of RAM on a combined box; `max_connections` 300; slow query log on; `sql_mode` compatible with the POS (non-strict, as the POS sets `stricton = FALSE`) **(verify)** |
| Composer / Git / SSH | Required on the server for the platform (SSH key login only) |
| Outbound network | HTTPS for mail/SMS providers. The vendor location-check call is removed by the Kaabe patch. |

### 9.3 Domains and SSL

```
<domain>                  marketing / landing (optional)
admin.<domain>            Kaabe Platform   → /home/<cp>/kaabe/platform/current/public
<tenant>.<domain>         Kaabe POS        → /home/<cp>/kaabe/pos/current
deeq.<domain>             tenant #1 (subdomain to be confirmed)
```

- **Option A – per-tenant subdomain (default, no extra cost).** Provisioning calls cPanel UAPI `SubDomain::addsubdomain` (document root = POS), then `SSL::start_autossl_check`. The certificate is usually issued within minutes to hours; the tenant is marked "SSL pending" until then. cPanel AutoSSL does **not** issue wildcard certificates.
- **Option B – wildcard.** Add a `*.<domain>` subdomain in cPanel plus a wildcard certificate (commercial, or Let's Encrypt through a DNS-01 client on the VPS). New tenants then work instantly. Recommended once tenant volume grows.
- **Either way:** HTTPS only, HSTS header, HTTP → HTTPS redirect.

### 9.4 Database creation on cPanel

On cPanel, database and user names must carry the account prefix, and a plain `CREATE DATABASE` from the app is not the supported path. The provisioner therefore uses **cPanel UAPI** through a restricted **cPanel API token** (encrypted in `.env`):

```
Mysql::create_database   name=<cp>_kb0002
Mysql::create_user       name=<cp>_kb0002 password=<random 32>
Mysql::set_privileges_on_database  user=<cp>_kb0002 database=<cp>_kb0002 privileges=ALL PRIVILEGES
```

- **Naming limits.** MariaDB allows usernames up to 47 characters including the prefix, and database names up to 63 with the prefix. The `kbNNNN` suffix stays well within both.
- **Visibility.** Databases created through UAPI appear in cPanel and phpMyAdmin, which helps support staff.

### 9.5 Directory layout

```
/home/<cp>/
  kaabe/
    platform/
      releases/2026xxxx-hhmm/   Laravel release
      current -> releases/…      (symlink, atomic deploys)
      shared/.env, storage/
    pos/
      releases/…                 PHP POS 19.1 + kaabe/ patch set
      current -> releases/…
      shared/.env                KAABE_REGISTRY_KEY, CRON_KEY, encryption key
    shared/
      tenants.php                registry (written by platform, 0640)
    backups/                     local backup staging (rotated)
  public_html/                   landing page (optional)
```

### 9.6 Cron jobs (cPanel → Cron Jobs)

| Schedule | Command | Does |
|---|---|---|
| `* * * * *` | `cd ~/kaabe/platform/current && php artisan schedule:run >> /dev/null 2>&1` | Laravel scheduler (everything below) |
| (scheduler) every minute | `queue:work --stop-when-empty --max-time=50` | Processes queued jobs (provisioning, sync, notifications) |
| every 5 min | `kaabe:sync --type=sales` | `SyncSales` + `SyncSaleEvents` for all active tenants |
| hourly | `kaabe:sync --type=usage` | `SyncUsage` |
| every 15 min | `kaabe:health` | Tenant health checks |
| daily 01:00 | `kaabe:billing:run` | Invoices due, overdue, grace, optional auto-suspend |
| daily 02:00 | `kaabe:backup:tenants` | Per-tenant dumps (§9.8) |
| daily 03:00 | `kaabe:entitlements:push --all` | Safety re-push |
| daily 04:00 | `model:prune`, `queue:prune-failed --hours=168` | Cleanup |
| per the POS's own schedule | `kaabe:pos-cron {recurring_payments|reports_mailer|ecommerce|quickbooks}` | Runs the **existing POS cron** for each tenant: `KAABE_TENANT=<id> php ~/kaabe/pos/current/index.php cron <method>` |

### 9.7 Environment variables

**Platform (`kaabe/platform/shared/.env`)**

```
APP_NAME="Kaabe POS"   APP_ENV=production   APP_DEBUG=false   APP_URL=https://admin.<domain>
APP_KEY=base64:…                      # back up securely – decrypts tenant secrets
DB_CONNECTION=mysql  DB_HOST=localhost  DB_DATABASE=<cp>_kaabe  DB_USERNAME=<cp>_kaabe  DB_PASSWORD=…
SESSION_DRIVER=database  SESSION_SECURE_COOKIE=true  SESSION_LIFETIME=30
QUEUE_CONNECTION=database   CACHE_STORE=database   (Redis later)
MAIL_MAILER=smtp  MAIL_HOST=…  MAIL_PORT=587  MAIL_USERNAME=…  MAIL_PASSWORD=…  MAIL_FROM_ADDRESS=no-reply@<domain>
KAABE_DOMAIN=<domain>
KAABE_POS_PATH=/home/<cp>/kaabe/pos/current
KAABE_REGISTRY_PATH=/home/<cp>/kaabe/shared/tenants.php
KAABE_REGISTRY_KEY=…                  # same value as POS
CPANEL_HOST=https://<server>:2083  CPANEL_USER=<cp>  CPANEL_API_TOKEN=…
KAABE_DB_PREFIX=<cp>_
KAABE_TRIAL_DAYS=14  KAABE_GRACE_DAYS=7  KAABE_AUTO_SUSPEND=false
BACKUP_REMOTE=…  BACKUP_REMOTE_KEY=…  BACKUP_RETENTION_DAILY=7  BACKUP_RETENTION_WEEKLY=4
SUPERADMIN_NAME="Zaki Ali"  SUPERADMIN_EMAIL=…
```

**POS (`kaabe/pos/shared/.env`, loaded by the Kaabe hook)**

```
CI_ENV=production   CI_BRANDING=kaabe   CI_LOG_THRESHOLD=1
KAABE_REGISTRY_PATH=/home/<cp>/kaabe/shared/tenants.php
KAABE_REGISTRY_KEY=…
KAABE_PLATFORM_URL=https://admin.<domain>
KAABE_ENCRYPTION_KEY=…                # CI3 encryption_key
CRON_KEY=…
```

No credentials remain in `application/config/database.php` (audit S2).

### 9.8 Backup configuration

| Layer | What | When | Retention |
|---|---|---|---|
| 1. InMotion | Server backups (per plan) | Provider schedule | Provider |
| 2. Kaabe per-tenant dumps | `mysqldump --single-transaction --routines --triggers` per tenant + central, gzip, checksum | Nightly | 7 daily + 4 weekly |
| 3. Off-server copy | Encrypted upload of layer 2 to S3-compatible storage (e.g. Backblaze B2 / Wasabi) | Nightly after layer 2 | 30 days |
| 4. Before every deploy or migration | Central + all affected tenants | Automatic in deploy script | Until the next successful deploy |
| 5. Restore drill | Restore one random tenant to a scratch database and compare row counts | Monthly (scheduled job + report) | — |

Backups include the `APP_KEY` and registry key in a separate, offline secret store. Without them, encrypted tenant credentials cannot be recovered.

### 9.9 Storage estimate

| Item | Estimate |
|---|---|
| Code (POS ≈ 220 MB unzipped, platform + vendor ≈ 150 MB) × 3 releases kept | ~1.1 GB |
| Tenant database (uploads live in the database) | **Estimate:** 50–500 MB each, depending on images and history. Measure tenant #1 during migration. |
| Local backups | ≈ 7 × total compressed database size (compression typically 5–10× for text-heavy data) |
| Logs | ~1 GB with rotation |

Tenant #1's real size (measured in migration step 2) is the basis for the first disk sizing.

### 9.10 Security configuration

- **Server:** firewall (CSF) with only 80/443 and SSH open, and SSH restricted to known IPs; ModSecurity on; cPanel/WHM 2FA on; phpMyAdmin not public.
- **cPanel API token:** scoped to the account.
- **Database users:** tenant users have no global privileges; the platform database user is limited to the central database.
- **Headers:** CSP, `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, HSTS.
- **Audit fixes S1–S7** applied to the POS before any tenant goes live.

### 9.11 Monitoring

- **External uptime checks** (e.g. UptimeRobot) on `admin.<domain>/health` and on tenant #1.
- **`/health` endpoint** checks the database, queue lag, last scheduler run and disk space.
- **Internal records:** `system_events` plus a Super Admin "System health" page showing failed jobs, sync lag per tenant, SSL status and backup status.
- **Error tracking** (optional: Sentry, free tier).

---

## 10. Data migration strategy for `ssgplatforms_deeq`

The principle is that **nothing in the live database is deleted or rewritten**. The migration only adds Kaabe tables and settings, and passwords upgrade themselves on login.

| Step | Action | Output / check |
|---|---|---|
| 0 | **Rotate** the exposed `ssgplatforms_deeq` database password (audit S2); take the credentials out of the code | New password in the POS `.env` only |
| 1 | **Inventory** (read-only): POS version from `phppos_migrations`, row count of every table, sum of `phppos_sales.total` by year, number of locations, employees, items, customers, database size | `inventory_before.json` |
| 2 | **Back up** with a cPanel full backup **and** `mysqldump --single-transaction`, stored off the server; test-restore it into a scratch database | Checksum + restore log |
| 3 | **Staging copy:** restore to `<cp>_deeq_staging`, run the patched POS against it on `deeq-staging.<domain>` | Staging site |
| 4 | **Upgrade the schema if behind:** if the latest migration is older than `20221115200805`, run the POS's own `Migrate` controller on staging and record the result | Migration log |
| 5 | **Add Kaabe objects:** `phppos_kaabe_location_meta` (every existing location → `kind=branch`), `phppos_kaabe_meta`, `app_config.kaabe_*`; seed the five role templates **without** reassigning existing employees; create the hidden `kaabe_support` employee | Additive only |
| 6 | **Register centrally:** `tenants` row KB-0001 "Deeq" (name to confirm), `pos_instances` (database `ssgplatforms_deeq` keeps its name), `branches` from `phppos_locations`, platform API key, owner = the employee you name (default: the original admin, person_id 1) | Central rows |
| 7 | **Plan and subscription:** you choose the plan; limits must be **≥ current usage** (checked automatically), or an override is added. Create the subscription manually with its start date. | `subscription_events` |
| 8 | **Backfill metrics:** compute `tenant_daily_metrics` for the full sales history | Totals by year match step 1 exactly |
| 9 | **Reconcile:** row counts per table equal step 1 (except new Kaabe tables); sales totals equal; spot-check 20 random sales, 20 items, stock of 20 items per location; log in as 3 existing users (MD5 → bcrypt upgrade works); run 5 key reports and compare with the old install | Signed reconciliation report |
| 10 | **Functional test on staging:** checkout, returns, receiving, transfer, expense, customer, reports, receipt printing, register open/close, API | Test checklist |
| 11 | **Cut-over (short maintenance window):** freeze the old install (maintenance page), take a final incremental dump, apply steps 4–5 to **production** `ssgplatforms_deeq`, switch the registry to live, point `deeq.<domain>` (and the old URL, via 301 redirect) to Kaabe POS | Live |
| 12 | **Rollback plan:** if checks fail within the window, point the old URL back to the untouched old install and restore the step 11 dump. Because steps 4–5 are additive, the old code still runs on the migrated database. | Rollback ≤ 30 min |

---

## 11. Open items to confirm before Phase 3

1. **Product domain.** For example `kaabepos.com`: is it registered, and should the DNS be on InMotion?
2. **Current InMotion plan.** Shared or VPS, and the server's current PHP and MariaDB versions. You can see these in cPanel under "Server Information".
3. **Tenant #1.** The business name, the subdomain (e.g. `deeq`), the owner's employee login, and the URL it runs on today.
4. **Tenant #1 version.** Is the live install the same 19.1 code as the zip you sent? If it has been modified, send those changes.
5. **Tenant #1 plan.** Which plan it gets (for example Enterprise, complimentary).

## 12. What Phase 3 will deliver, in order

1. The Kaabe patch set for the POS: tenant resolver, entitlement and limit gates, security fixes S1–S7, branding, warehouse meta, support login, business dashboard, Account page, with every change listed in `kaabe/PATCHES.md`.
2. Central database update (the §3.3 changes) and tenant Kaabe migrations.
3. Provisioner (cPanel UAPI + POS API) and registry writer.
4. Tenant #1 migration scripts (§10), run against staging.

---

*Hosting facts are from InMotion Hosting and cPanel documentation, checked September 2026: AutoSSL wildcard limits, wildcard installation on VPS or Dedicated only, UAPI database functions and naming limits, MariaDB on InMotion servers.*
