> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quinnsambal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi tenant

<style>
  {`
    [class*="max-w-none"][class*="table"] {
      display: block !important;
      overflow-x: auto !important;
      max-width: 100% !important;
      width: 100% !important;
      flex-grow: 0 !important;
    }
    [class*="max-w-none"][class*="table"] > table {
      width: 100% !important;
      max-width: 100% !important;
      table-layout: fixed !important;
    }
    .mermaid {
      max-width: 100% !important;
      overflow-x: auto !important;
    }
    article svg[role="img"] {
      max-width: 100% !important;
      height: auto !important;
    }
    article img, .prose img {
      max-width: 100% !important;
      height: auto !important;
    }
    img[src*="LOGO"], img[src*="logo"] {
      max-width: 120px !important;
      max-height: 40px !important;
      width: auto !important;
      height: auto !important;
      object-fit: contain !important;
    }
    `}
</style>

***

title: "Multi-Tenant Architecture"
description: "Arsitektur multi-tenant: CompanySwitcher 893 baris, quadruple persistence, subdomain detection, plan-based slot limits, dan data isolation."
----------------------------------------------------------------------------------------------------------------------------------------------------------

# Multi-Tenant Architecture

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/core/workspaces.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=eba81a4b1998a18eb83e45696cbc8961" alt="Multi-Tenant" width="1920" height="1080" data-path="docs/mintlify/screenshots/core/workspaces.png" />

Quinn of Spicy menggunakan arsitektur multi-tenant yang memungkinkan satu sistem digunakan oleh multiple perusahaan (tenants) dengan isolasi data penuh. CompanySwitcher.jsx (893 baris) adalah komponen utama yang menangani switching antar company dengan quadruple persistence pattern, plan-based slot limits, dan real-time synchronization.

Sistem ini mendukung subdomain-based tenant detection untuk custom domain (erp.snishop.com, snishop.id, apotekpro.id) dengan fallback ke selector UI. Setiap entity yang company-specific memiliki field `company_id` yang otomatis di-filter di setiap query, memastikan data isolation 100%.

Multi-tenant architecture ini cocok untuk franchise, holding company, consultant/agency, dan SaaS platform yang melayani multiple bisnis.

## Arsitektur Komponen

```mermaid theme={null}
graph TD
    A[CompanySwitcher.jsx<br/>893 lines] --> B[Quadruple Persistence]
    A --> C[Plan-Based Slot Limits]
    A --> D[Subdomain Detection<br/>tenantHelper.js]
    A --> E[Real-time Sync<br/>BroadcastChannel]
    
    B --> F[localStorage]
    B --> G[sessionStorage]
    B --> H[Cloud DB<br/>user.active_company_id]
    B --> I[globalCache]
    
    C --> J[free: 1 slot]
    C --> K[pro: 0 slots]
    C --> L[business: 3 slots]
    C --> M[advanced: 10 slots]
    C --> N[enterprise: 20 slots]
    C --> O[corporate: 10 slots]
    
    D --> P[erp.snishop.com]
    D --> Q[snishop.id]
    D --> R[apotek.id]
    
    E --> S[memberJoinedCompany]
    E --> T[companyChanged]
```

## Entity & Model

### Company

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `name` | String | Nama company |
| `owner_email` | String | Email owner |
| `logo_url` | String | URL logo |
| `subscription_plan` | Enum | `free`, `pro`, `business`, `advanced`, `enterprise`, `corporate` |
| `company_slots` | Number | Jumlah slot company yang bisa diakses |
| `allow_company_features` | Boolean | Flag untuk allow/blok company features |
| `created_at` | Timestamp | Waktu pembuatan |

### User (Multi-Tenant Fields)

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `email` | String | Email user |
| `active_company_id` | UUID | Company yang sedang aktif |
| `company_ids` | Array | Daftar company yang bisa diakses |
| `role` | String | Role di platform level |

### CompanyMember

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `user_id` | UUID | Reference ke User |
| `company_id` | UUID | Reference ke Company |
| `role` | Enum | Role di company level |
| `status` | Enum | `active`, `expired`, `suspended` |

## Fitur Utama

### 1. Quadruple Persistence Pattern

Company context disimpan di 4 layer untuk memastikan durability:

| Layer | Key | Deskripsi |
| - | - | - |
| localStorage | `active_company_id` | Persist antar session |
| sessionStorage | `active_company_id` | Persist dalam tab |
| Cloud DB | `user.active_company_id` | Persist antar device |
| globalCache | `activeCompanyId` | In-memory fast access |

**Resolution Priority**:

```mermaid theme={null}
flowchart TD
    A[Resolve Active Company] --> B{globalCache has value?}
    B -->|Yes| C[Use globalCache]
    B -->|No| D{localStorage has value?}
    D -->|Yes| E[Use localStorage]
    D -->|No| F{sessionStorage has value?}
    F -->|Yes| G[Use sessionStorage]
    F -->|No| H{Cloud DB has value?}
    H -->|Yes| I[Use Cloud DB]
    H -->|No| J[Use first company from list]
```

### 2. Plan-Based Slot Limits

Setiap subscription plan memiliki batas jumlah company yang bisa diakses:

| Plan | Company Slots | Deskripsi |
| - | - | - |
| Free | 1 | Hanya 1 company |
| Pro | 0 | Tidak bisa buat company (note: CompanySwitcher sekarang allow free dengan 1 slot) |
| Business | 3 | Maksimum 3 companies |
| Advanced | 10 | Maksimum 10 companies |
| Enterprise | 20 | Maksimum 20 companies |
| Corporate | 10 | Maksimum 10 companies |

### 3. Subdomain Detection

`tenantHelper.js` (80 baris) mendeteksi tenant dari subdomain:

**Base Domains**:

| Domain | Deskripsi |
| - | - |
| `erp.snishop.com` | Main ERP domain |
| `snishop.id` | Indonesian domain |
| `apotekpro.id` | Pharmacy-specific domain |
| `localhost` | Development |

**Detection Logic**:

```mermaid theme={null}
flowchart TD
    A[Get hostname] --> B{Is preview env?<br/>base44.app, vercel.app, netlify.app}
    B -->|Yes| C[Return null — no tenant resolution]
    B -->|No| D{Has subdomain?}
    D -->|No| E[Return null]
    D -->|Yes| F{Is dev override?<br/>URL param or localStorage}
    F -->|Yes| G[Use override value]
    F -->|No| H[Extract subdomain]
    H --> I[sanitizeSubdomain]
    I --> J[Return tenant ID]
```

**Excluded Environments**: Preview environments (base44.app, vercel.app, netlify.app, hostinger.com) tidak melakukan tenant resolution untuk mencegah accidental data leakage.

### 4. Company Switching Flow

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant CS as CompanySwitcher
    participant LS as localStorage
    participant SS as sessionStorage
    participant DB as Cloud DB
    participant GC as globalCache
    participant BC as BroadcastChannel

    U->>CS: Select company from dropdown
    CS->>CS: Validate slot limit
    CS->>LS: Set active_company_id
    CS->>SS: Set active_company_id
    CS->>DB: Update user.active_company_id
    CS->>GC: Set activeCompanyId
    CS->>BC: Broadcast 'companyChanged'
    CS->>CS: Reload page with new context
    
    Note over BC: Other tabs receive broadcast
    BC-->>CS: Other tabs refresh data
```

### 5. Data Isolation

Setiap entity yang company-specific memiliki field `company_id`:

**Automatic Filtering**:

```mermaid theme={null}
flowchart LR
    A[User Query] --> B[Server Function]
    B --> C[Get current_company_id]
    C --> D[Add WHERE company_id = ?]
    D --> E[Execute Query]
    E --> F[Return Filtered Data]
```

**Statistics**:

| Metric | Value |
| - | - |
| Entities dengan company\_id | 109 dari 162 (67%) |
| Data isolation | 100% |
| Cross-company queries | Blocked |
| Max companies per user | Unlimited (depends on plan) |

### 6. Company Creation

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant CS as CompanySwitcher
    participant SF as Server Function
    participant DB as Database

    U->>CS: Click "Create New Company"
    CS->>CS: Check slot availability
    CS->>CS: Generate idempotency key
    CS->>SF: createCompanyWorkspace(name, idempotency_key)
    SF->>DB: Insert Company record
    SF->>DB: Insert CompanyMember (role=owner)
    SF->>DB: Update user.company_ids
    DB-->>SF: Return created company
    SF-->>CS: Return company + membership
    CS->>CS: Switch to new company
    CS-->>U: Show new company dashboard
```

**Idempotency Key**: Mencegah duplikasi company creation akibat network retry atau double-click.

### 7. Demo Mode Support

CompanySwitcher mendukung demo mode untuk trial experience:

| Condition | Behavior |
| - | - |
| `isDemoMode()` = true | Gunakan `DEMO_COMPANY` dari demoModeManager |
| `isDemoMode()` = false | Fetch dari `listAccessibleCompanies` server function |

### 8. Real-time Synchronization

CompanySwitcher listen ke multiple events untuk real-time updates:

| Event | Source | Aksi |
| - | - | - |
| `memberJoinedCompany` | BroadcastChannel | Refresh company list |
| `companyChanged` | BroadcastChannel | Update UI |
| 30-second interval | Timer | Auto-refresh data |

## Use Cases

### 1. Franchise

Satu sistem untuk multiple outlet/franchise:

```mermaid theme={null}
graph TD
    A[Headquarters<br/>PT Quinn of Spicy] --> B[Outlet Jakarta<br/>Company A]
    A --> C[Outlet Surabaya<br/>Company B]
    A --> D[Outlet Bandung<br/>Company C]
    
    B --> E[Data A<br/>Isolated]
    C --> F[Data B<br/>Isolated]
    D --> G[Data C<br/>Isolated]
    
    A --> H[HQ Dashboard<br/>See All Outlets]
```

* Tiap outlet = 1 company
* Headquarter bisa lihat semua outlet
* Tiap outlet punya data terpisah

### 2. Holding Company

Parent company dengan multiple subsidiaries:

* Parent = 1 company
* Subsidiaries = multiple companies
* Parent bisa akses semua subsidiaries
* Subsidiaries hanya lihat data sendiri

### 3. Consultant / Agency

Konsultan yang handle multiple clients:

* Consultant = 1 user
* Clients = multiple companies
* Consultant bisa switch antar client
* Tiap client data terpisah

## Security

### 1. Data Isolation

* Enforced di database level
* Automatic filtering di semua queries
* Cross-company access = error + alert

### 2. Audit Trail

Setiap akses ke data company lain dicatat:

| Field | Deskripsi |
| - | - |
| Who | User ID, email |
| Which company | Target company |
| When | Timestamp |
| IP address | User IP |
| Result | Blocked/allowed |

### 3. Subdomain Sanitization

`tenantHelper.js` melakukan sanitization untuk mencegah injection:

* Remove special characters
* Lowercase normalization
* Length validation
* Reserved name check

## Integrasi Cross-Module

```mermaid theme={null}
graph LR
    A[Multi-Tenant System] --> B[CompanySwitcher<br/>UI switching]
    A --> C[tenantHelper<br/>Subdomain detection]
    A --> D[ERPAccessGuard<br/>Permission check]
    A --> E[CompanySettings<br/>Owner config]
    A --> F[RoleManager<br/>Member management]
    
    B --> G[Quadruple persistence]
    C --> H[Base domains]
    D --> I[Company-scoped routes]
    E --> J[Owner verification]
    F --> K[Role-based access]
```

## Troubleshooting

### User tidak bisa switch company

**Penyebab**: User belum di-assign ke company lain atau slot limit tercapai
**Solusi**: Admin assign user ke company yang dimaksud atau upgrade plan

### Data tidak muncul setelah switch

**Penyebab**: Company baru belum punya data
**Solusi**: Create data di company baru, atau import dari company lama

### Error "Access denied to company X"

**Penyebab**: User tidak punya akses ke company X
**Solusi**: Admin assign user ke company X dengan role yang sesuai

### Subdomain tidak terdeteksi

**Penyebab**: Domain termasuk preview environment atau subdomain tidak valid
**Solusi**: Check base domain list dan gunakan dev override via URL param

## Tips

* Quadruple persistence memastikan company context survive browser restart dan sync antar device
* Plan-based slot limits mencegah abuse — upgrade plan jika butuh lebih banyak company
* Subdomain detection otomatis untuk custom domain — tidak perlu manual config
* BroadcastChannel sync memastikan semua tab mendapat update real-time saat switch company
* Idempotency key mencegah duplikasi company creation — aman dari network retry
* Demo mode berguna untuk onboarding — calon user bisa trial tanpa commitment

***

## Referensi Entity Schema (base44)

Dokumentasi schema lengkap untuk setiap entity yang terlibat dalam arsitektur multi-tenant, diekstrak langsung dari file definisi JSONC di `base44/entities/`.

### Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    User ||--o{ Company : "membuat / memiliki"
    User ||--o{ CompanyMember : "terdaftar sebagai"
    User ||--o{ WorkspaceMember : "bergabung ke"
    User ||--o{ Subscription : "memiliki langganan"
    User ||--o{ CompanyInvitation : "menerima undangan"

    Company ||--o{ CompanyMember : "memiliki anggota"
    Company ||--o{ CompanyInvitation : "mengirim undangan"
    Company ||--o{ Workspace : "menampung workspace"
    Company ||--o{ Subscription : "memiliki langganan company"

    Workspace ||--o{ WorkspaceMember : "memiliki anggota"

    User {
        UUID id PK
        String email UK
        String full_name
        String role "admin | user"
        String subscription_plan "free | pro | business | advanced | enterprise"
        UUID active_company_id FK
        Number company_slots_purchased
        String admin_type "owner | basic"
        String admin_tier "none | business | advanced | enterprise"
        Number balance
        Number commission_balance
    }

    Company {
        UUID id PK
        String name
        UUID owner_id FK
        String owner_email
        String subscription_plan "free | pro | business | advanced | enterprise"
        String industry
        String description
        String address
        String phone
        String email
        String website
        String logo_url
        String tax_id
        Number employee_count
        String business_type
        String active_modules
        Object settings
        Object landing_page_config
        Object metadata
    }

    CompanyMember {
        UUID id PK
        UUID company_id FK
        UUID user_id FK
        String user_email
        String user_name
        String role "owner | admin | supervisor | ..."
        String employee_id
        String department
        String position
        String status "active | inactive | pending"
        Date joined_date
        String invited_by
        Object permissions
        Array assigned_locations
        Object working_hours
        Number salary
        String notes
    }

    CompanyInvitation {
        UUID id PK
        UUID company_id FK
        String company_name
        String invited_email
        String invited_by
        String invited_by_name
        String role
        String department
        String position
        String description
        Object permissions
        String message
        String status "pending | accepted | rejected | expired | cancelled"
        DateTime expires_at
        DateTime accepted_at
        DateTime rejected_at
        DateTime cancelled_at
    }

    Workspace {
        UUID id PK
        UUID company_id FK
        String name
        String description
        String icon
        String color
        UUID owner_id FK
        Boolean is_personal
        Object settings
    }

    WorkspaceMember {
        UUID id PK
        UUID workspace_id FK
        UUID user_id FK
        String role "owner | admin | member | viewer"
        String invited_by
        DateTime joined_at
        String description
        Object permissions
    }

    Subscription {
        UUID id PK
        UUID user_id FK
        UUID company_id FK
        String service_name
        Number cost
        String currency
        String billing_cycle "weekly | monthly | quarterly | yearly"
        Date next_due_date
        Date start_date
        Date end_date
        String status "active | paused | cancelled"
        String payment_method
        Boolean auto_renew
        String notes
    }
```

***

### Schema Detail per Entity

#### Company

Entity utama yang merepresentasikan satu tenant/perusahaan dalam sistem multi-tenant.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `name` | String | Ya | — | Nama perusahaan |
| `owner_id` | UUID | Ya | — | ID owner perusahaan (referensi ke User) |
| `owner_email` | String | Ya | — | Email owner perusahaan |
| `owner_subscription_plan` | Enum | Tidak | — | Plan membership owner saat ini: `free`, `pro`, `business`, `advanced`, `enterprise` |
| `description` | String | Tidak | — | Deskripsi perusahaan |
| `industry` | Enum | Tidak | — | Sektor industri: `retail`, `manufacturing`, `services`, `technology`, `food_beverage`, `healthcare`, `education`, `other` |
| `address` | String | Tidak | — | Alamat lengkap perusahaan |
| `phone` | String | Tidak | — | Nomor telepon perusahaan |
| `email` | String | Tidak | — | Email perusahaan |
| `website` | String | Tidak | — | Website perusahaan |
| `logo_url` | String | Tidak | — | URL logo perusahaan |
| `tax_id` | String | Tidak | — | NPWP perusahaan |
| `employee_count` | Number | Tidak | `0` | Jumlah karyawan |
| `business_type` | String | Tidak | — | Kategori bisnis yang dipilih saat onboarding |
| `active_modules` | String | Tidak | — | JSON string berisi array ID modul aktif |
| `metadata` | Object | Tidak | — | Data tambahan / legacy |
| `landing_page_config` | Object | Tidak | — | Konfigurasi landing page perusahaan (tema, konten, pembayaran, dll) |
| `settings` | Object | Tidak | — | Pengaturan perusahaan (jam kerja, kebijakan cuti, ambang batas kedaluwarsa, strategi batch, pajak, spoilage) |

**Sub-field `settings`**:

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `settings.working_hours_start` | String | `"09:00"` | Jam mulai kerja |
| `settings.working_hours_end` | String | `"17:00"` | Jam akhir kerja |
| `settings.working_days` | Array\[Number] | `[1,2,3,4,5]` | Hari kerja (1=Senin, 7=Minggu) |
| `settings.leave_policy` | Object | — | Kebijakan cuti karyawan |
| `settings.expiry_thresholds` | Object | — | Ambang batas peringatan kedaluwarsa lot (dalam hari) |
| `settings.expiry_thresholds.critical` | Number | `7` | Hari kritis sebelum kedaluwarsa |
| `settings.expiry_thresholds.warning` | Number | `30` | Hari peringatan sebelum kedaluwarsa |
| `settings.expiry_thresholds.info` | Number | `90` | Hari informasi sebelum kedaluwarsa |
| `settings.batch_allocation_strategy` | Enum | `"fifo"` | Strategi alokasi batch: `fifo` (first-in-first-out) atau `fefo` (first-expiry-first-out) |
| `settings.tax.enabled` | Boolean | `false` | Apakah pajak diaktifkan |
| `settings.tax.rate` | Number | `11` | Tarif PPN dalam persen |
| `settings.tax.mode` | Enum | `"inclusive"` | Mode pajak: `inclusive` (sudah termasuk) atau `exclusive` (ditambahkan ke harga) |
| `settings.tax.rounding` | Enum | `"per_transaction"` | Pembulatan pajak: `per_line` atau `per_transaction` |
| `settings.spoilage_default_treatment` | Enum | `"absorbed_normal"` | Perlakuan spoilage: `absorbed_normal` (masuk HPP) atau `expense_abnormal` (beban periode) |

#### User

Entity pengguna sistem dengan field-field multi-tenant yang memungkinkan satu user mengakses beberapa company.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `email` | String | Ya | — | Email pengguna (unik) |
| `full_name` | String | Ya | — | Nama lengkap pengguna |
| `role` | Enum | Tidak | — | Role di platform level: `admin`, `user` |
| `subscription_plan` | Enum | Tidak | `"free"` | Plan langganan: `free`, `pro`, `business`, `advanced`, `enterprise` |
| `subscription_start` | DateTime | Tidak | — | Tanggal mulai langganan |
| `subscription_end` | DateTime | Tidak | — | Tanggal akhir langganan |
| `membership_duration_type` | Enum | Tidak | — | Tipe durasi membership: `monthly`, `yearly`, `custom`, `lifetime` |
| `membership_start_date` | Date | Tidak | — | Tanggal mulai membership |
| `membership_end_date` | Date | Tidak | — | Tanggal berakhir membership |
| `is_readonly_mode` | Boolean | Tidak | `false` | Mode read-only ketika expired dalam grace period 3 hari |
| `trial_end` | DateTime | Tidak | — | Tanggal akhir trial |
| `active_company_id` | UUID | Tidak | — | ID company yang sedang aktif (multi-tenant) |
| `company_slots_purchased` | Number | Tidak | `0` | Slot tambahan yang dibeli di luar batas plan |
| `admin_type` | Enum | Tidak | — | Tipe admin aplikasi: `owner` (full access) atau `basic` (hanya transaksi produk digital) |
| `admin_tier` | Enum | Tidak | `"none"` | Tier admin untuk company management: `none`, `business`, `advanced`, `enterprise` |
| `ai_monthly_usage` | Number | Tidak | `0` | Penggunaan AI bulanan |
| `ai_credits` | Number | Tidak | `10` | Kredit AI yang tersedia |
| `ai_addon_quota` | Number | Tidak | `0` | Kuota addon AI |
| `storage_used` | Number | Tidak | `0` | Penyimpanan yang digunakan |
| `balance` | Number | Tidak | `0` | Saldo deposit untuk membeli membership/addon |
| `commission_balance` | Number | Tidak | `0` | Saldo komisi referral yang dapat ditarik |
| `admin_commission_balance` | Number | Tidak | `0` | Saldo komisi admin basic dari transaksi |
| `total_earnings` | Number | Tidak | `0` | Total komisi yang pernah diterima (akumulasi) |
| `productivity_score` | Number | Tidak | `0` | Skor produktivitas pengguna |
| `current_streak` | Number | Tidak | `0` | Streak hari berturut-turut menyelesaikan tugas |
| `longest_streak` | Number | Tidak | `0` | Streak terpanjang |
| `user_level` | Number | Tidak | `1` | Level pengguna berdasarkan achievement points |
| `preferences` | Object | Tidak | — | Preferensi pengguna (tema, warna, wallpaper, dll) |

#### CompanyMember

Entity yang merepresentasikan keanggotaan seorang user dalam suatu company. Setiap baris adalah relasi many-to-many antara User dan Company dengan role dan permission tersendiri.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `company_id` | UUID | Ya | — | Referensi ke Company |
| `user_id` | UUID | Tidak | — | Referensi ke User |
| `user_email` | String | Ya | — | Email pengguna (digunakan sebelum user\_id ter-link) |
| `user_name` | String | Tidak | — | Nama pengguna |
| `role` | Enum | Tidak | `"employee"` | Role dalam perusahaan (lihat tabel enum di bawah) |
| `employee_id` | String | Tidak | — | Link ke entity Employee |
| `department` | String | Tidak | — | Departemen pengguna |
| `position` | String | Tidak | — | Jabatan pengguna |
| `status` | Enum | Tidak | `"active"` | Status keanggotaan: `active`, `inactive`, `pending` |
| `joined_date` | Date | Tidak | — | Tanggal bergabung |
| `invited_by` | String | Tidak | — | Email yang mengundang |
| `permissions` | Object | Tidak | — | Hak akses detail untuk member (lihat tabel RBAC di bawah) |
| `assigned_locations` | Array\[String] | Tidak | — | Daftar ID lokasi gudang/outlet yang diizinkan (RBAC-01) |
| `working_hours` | Object | Tidak | — | Jam kerja karyawan (`start`, `end`) |
| `salary` | Number | Tidak | — | Gaji karyawan (opsional) |
| `notes` | String | Tidak | — | Catatan tambahan |

#### CompanyInvitation

Entity undangan untuk bergabung ke suatu company. Mengikuti lifecycle invitation: dibuat -> diterima/ditolak/kedaluwarsa.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `company_id` | UUID | Ya | — | Referensi ke Company yang mengundang |
| `company_name` | String | Tidak | — | Nama company (denormalized untuk tampilan) |
| `invited_email` | String | Ya | — | Email calon anggota yang diundang |
| `invited_by` | String | Ya | — | Email pengguna yang mengirim undangan |
| `invited_by_name` | String | Tidak | — | Nama pengundang (denormalized) |
| `role` | Enum | Tidak | `"employee"` | Role yang akan diberikan saat diterima |
| `department` | String | Tidak | — | Departemen yang akan di-assign |
| `position` | String | Tidak | — | Jabatan yang akan di-assign |
| `description` | String | Tidak | — | Pesan/keterangan tambahan dalam undangan (maks 1000 karakter) |
| `permissions` | Object | Tidak | — | Hak akses yang akan diberikan (sama dengan CompanyMember.permissions) |
| `message` | String | Tidak | — | Pesan pribadi pengundang |
| `status` | Enum | Tidak | `"pending"` | Status undangan: `pending`, `accepted`, `rejected`, `expired`, `cancelled` |
| `expires_at` | DateTime | Tidak | — | Waktu kedaluwarsa undangan |
| `accepted_at` | DateTime | Tidak | — | Waktu undangan diterima |
| `rejected_at` | DateTime | Tidak | — | Waktu undangan ditolak |
| `cancelled_at` | DateTime | Tidak | — | Waktu undangan dibatalkan |

#### Workspace

Entity workspace yang mendukung scoped task/note management dalam suatu company. Workspace bisa bersifat company-scoped atau personal.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `company_id` | UUID | Tidak | — | ID perusahaan (null untuk personal workspace) |
| `name` | String | Ya | — | Nama workspace |
| `description` | String | Tidak | — | Deskripsi workspace |
| `icon` | String | Tidak | — | Icon workspace |
| `color` | String | Tidak | `"#2563eb"` | Warna tema workspace |
| `owner_id` | UUID | Ya | — | ID pemilik workspace |
| `is_personal` | Boolean | Tidak | `false` | Apakah workspace pribadi |
| `settings` | Object | Tidak | — | Pengaturan workspace |

**Sub-field `settings`**:

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `settings.allow_public_sharing` | Boolean | `false` | Izinkan berbagi publik |
| `settings.default_task_priority` | Enum | `"medium"` | Prioritas task default: `low`, `medium`, `high`, `urgent` |

#### WorkspaceMember

Entity keanggotaan dalam workspace. Setiap baris adalah relasi many-to-many antara User dan Workspace.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `workspace_id` | UUID | Ya | — | Referensi ke Workspace |
| `user_id` | UUID | Ya | — | Referensi ke User |
| `role` | Enum | Tidak | `"member"` | Peran dalam workspace: `owner`, `admin`, `member`, `viewer` |
| `invited_by` | String | Tidak | — | Email pengguna yang mengundang |
| `joined_at` | DateTime | Tidak | — | Waktu bergabung |
| `description` | String | Tidak | — | Catatan tambahan mengenai anggota (maks 1000 karakter) |
| `permissions` | Object | Tidak | — | Hak akses di level workspace |

**Sub-field `permissions`**:

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `permissions.can_create_tasks` | Boolean | `true` | Bisa membuat task |
| `permissions.can_edit_tasks` | Boolean | `true` | Bisa mengedit task |
| `permissions.can_delete_tasks` | Boolean | `false` | Bisa menghapus task |
| `permissions.can_invite_members` | Boolean | `false` | Bisa mengundang anggota |
| `permissions.can_access_all_tasks` | Boolean | `false` | Akses ke semua task terlepas dari assignment |

#### Subscription

Entity langganan yang bisa melekat ke user (personal) atau ke company.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | Ya | Auto | Primary key |
| `user_id` | UUID | Ya | — | ID pengguna pemilik langganan |
| `company_id` | UUID | Tidak | — | ID perusahaan (null untuk personal) |
| `service_name` | String | Ya | — | Nama layanan/produk berlangganan |
| `cost` | Number | Ya | — | Biaya langganan per periode |
| `currency` | String | Tidak | `"IDR"` | Mata uang |
| `billing_cycle` | Enum | Tidak | `"monthly"` | Siklus penagihan: `weekly`, `monthly`, `quarterly`, `yearly` |
| `next_due_date` | Date | Ya | — | Tanggal jatuh tempo pembayaran berikutnya |
| `start_date` | Date | Tidak | — | Tanggal mulai langganan |
| `end_date` | Date | Tidak | — | Tanggal akhir langganan (jika ada) |
| `status` | Enum | Tidak | `"active"` | Status langganan: `active`, `paused`, `cancelled` |
| `payment_method` | String | Tidak | — | Metode pembayaran |
| `auto_renew` | Boolean | Tidak | `true` | Apakah otomatis diperpanjang |
| `notes` | String | Tidak | — | Catatan tambahan |

***

### Diagram Status: Sikapus Undangan Company

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: Undangan dibuat

    pending --> accepted: Undangan diterima oleh user
    pending --> rejected: Undangan ditolak oleh user
    pending --> expired: Melewati expires_at
    pending --> cancelled: Dibatalkan oleh pengundang

    accepted --> [*]
    rejected --> [*]
    expired --> [*]
    cancelled --> [*]

    state pending {
        [*] --> MenungguRespon
        MenungguRespon : invited_email menerima\nemail notifikasi
        MenungguRespon : Status = pending
    }

    state accepted {
        [*] --> CompanyMemberDibuat
        CompanyMemberDibuat : user_email di-link ke user_id
        CompanyMemberDibuat : role dan permissions\ndi-copy dari invitation
    }
```

### Diagram Status: Keanggotaan CompanyMember

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: Diundang
    pending --> active: Undangan diterima
    active --> inactive: Dinonaktifkan oleh admin/owner
    inactive --> active: Diaktifkan kembali
    inactive --> [*]: Dihapus dari company

    state active {
        [*] --> DapatAkses
        DapatAkses : company_id ter-filter\ndi semua query
        DapatAkses : permissions ditegakkan\noleh ERPAccessGuard
    }

    state inactive {
        [*] --> AksesDiblokir
        AksesDiblokir : Tidak bisa login\nke company ini
        AksesDiblokir : Data tetap ada\ntetapi tidak bisa diakses
    }
```

### Diagram Status: Langganan Subscription

```mermaid theme={null}
stateDiagram-v2
    [*] --> active: Langganan dimulai

    active --> paused: Ditangguhkan sementara
    active --> cancelled: Dibatalkan
    active --> [*]: Berakhir dan dihapus

    paused --> active: Diaktifkan kembali
    paused --> cancelled: Dibatalkan saat pause
    cancelled --> [*]

    state active {
        [*] --> LayananPenuh
        LayananPenuh : Semua fitur tersedia\nsesuai plan
        LayananPenuh : auto_renew memperpanjang\nsecara otomatis
    }

    state paused {
        [*] --> LayananTerbatas
        LayananTerbatas : Fitur dibatasi\nhanya read-only
        LayananTerbatas : Data tetap aman\ntidak dihapus
    }
```

***

### Diagram Sekuens: Undangan & Penerimaan Member Baru

```mermaid theme={null}
sequenceDiagram
    participant Admin as Admin/Owner
    participant CI as CompanyInvitation
    participant Email as Notifikasi Email
    participant User as Calon Member
    participant CM as CompanyMember
    participant DB as Database

    Admin->>CI: Buat undangan (email, role, permissions)
    CI->>DB: Insert CompanyInvitation (status=pending)
    CI->>Email: Kirim notifikasi undangan
    Email-->>User: Email undangan bergabung

    alt Undangan Diterima
        User->>CI: Accept invitation
        CI->>CI: Update status = accepted, accepted_at = now()
        CI->>CM: Buat CompanyMember baru
        CM->>DB: Insert CompanyMember (role, permissions dari invitation)
        CM->>DB: Update user.active_company_id jika perlu
        CM-->>User: Redirect ke dashboard company
    else Undangan Ditolak
        User->>CI: Reject invitation
        CI->>DB: Update status = rejected, rejected_at = now()
    else Undangan Kedaluwarsa
        CI->>CI: Cron check expires_at
        CI->>DB: Update status = expired
    end
```

### Diagram Sekuens: Pembuatan Workspace dalam Company

```mermaid theme={null}
sequenceDiagram
    participant User as User (Owner/Admin)
    participant WS as Workspace Service
    participant WM as WorkspaceMember
    participant DB as Database
    participant BC as BroadcastChannel

    User->>WS: Buat workspace baru (nama, company_id)
    WS->>WS: Validasi user memiliki akses ke company_id
    WS->>DB: Insert Workspace (company_id, owner_id, settings)
    WS->>WM: Buat WorkspaceMember (role=owner)
    WM->>DB: Insert WorkspaceMember
    WS->>BC: Broadcast 'workspaceCreated'
    WS-->>User: Return workspace + membership

    opt Tambahkan Anggota
        User->>WM: Invite member (user_id, role)
        WM->>DB: Insert WorkspaceMember (workspace_id, user_id, role, permissions)
        WM->>BC: Broadcast 'memberJoinedWorkspace'
    end
```

### Diagram Sekuens: Switch Company dengan Quadruple Persistence

```mermaid theme={null}
sequenceDiagram
    participant User as User
    participant CS as CompanySwitcher
    participant GC as globalCache
    participant LS as localStorage
    participant SS as sessionStorage
    participant DB as Cloud DB
    participant BC as BroadcastChannel
    participant Tab as Tab Browser Lain

    User->>CS: Pilih company baru dari dropdown
    CS->>CS: Validasi slot limit (cek plan)

    alt Slot Tersedia
        CS->>GC: Set activeCompanyId = newCompanyId
        CS->>LS: Set active_company_id = newCompanyId
        CS->>SS: Set active_company_id = newCompanyId
        CS->>DB: Update user.active_company_id = newCompanyId
        CS->>BC: Broadcast 'companyChanged'
        CS->>CS: Reload halaman dengan konteks baru
        BC-->>Tab: Tab lain terima broadcast
        Tab->>Tab: Refresh data company
    else Slot Penuh
        CS-->>User: Tampilkan error "Slot limit tercapai, upgrade plan"
    end
```

***

### Tabel Enum Lengkap

#### Company.industry

| Nilai | Deskripsi |
| - | - |
| `retail` | Usaha ritel (toko, minimarket, supermarket) |
| `manufacturing` | Usaha manufaktur/pabrik |
| `services` | Usaha jasa/layanan |
| `technology` | Usaha teknologi/IT |
| `food_beverage` | Usaha makanan dan minuman (F\&B) |
| `healthcare` | Usaha kesehatan (klinik, apotek, RS) |
| `education` | Usaha pendidikan (sekolah, kursus) |
| `other` | Lainnya |

#### Company.owner\_subscription\_plan & User.subscription\_plan

| Nilai | Deskripsi |
| - | - |
| `free` | Plan gratis, fitur dasar |
| `pro` | Plan profesional, fitur lanjutan |
| `business` | Plan bisnis, multi-company (3 slot) |
| `advanced` | Plan advanced, multi-company (10 slot) |
| `enterprise` | Plan enterprise, multi-company (20 slot) |

#### CompanyMember.role

| Nilai | Deskripsi |
| - | - |
| `owner` | Pemilik perusahaan, akses penuh |
| `admin` | Administrator, akses hampir penuh |
| `supervisor` | Supervisor, mengawasi operasional |
| `store_admin` | Admin toko, mengelola data toko |
| `stock_admin` | Admin stok, mengelola inventaris |
| `finance_admin` | Admin keuangan, mengelola finansial |
| `hr_admin` | Admin HR, mengelola SDM |
| `transaction_admin` | Admin transaksi, mengelola transaksi |
| `employee` | Karyawan biasa, akses terbatas |
| `production_operator` | Operator produksi, mengelola batch produksi |
| `qc_inspector` | Inspector QC, mengelola quality control |
| `sales_marketing` | Sales & marketing, mengelola penjualan |
| `partner_distributor` | Partner/distributor, akses distribusi |

#### CompanyMember.status

| Nilai | Deskripsi |
| - | - |
| `active` | Anggota aktif, bisa mengakses company |
| `inactive` | Anggota nonaktif, akses diblokir |
| `pending` | Menunggu konfirmasi undangan |

#### CompanyInvitation.status

| Nilai | Deskripsi |
| - | - |
| `pending` | Undangan menunggu respon |
| `accepted` | Undangan diterima, CompanyMember dibuat |
| `rejected` | Undangan ditolak oleh calon member |
| `expired` | Undangan kedaluwarsa (melewati expires\_at) |
| `cancelled` | Undangan dibatalkan oleh pengundang |

#### User.role

| Nilai | Deskripsi |
| - | - |
| `admin` | Administrator platform, akses penuh ke semua fitur |
| `user` | Pengguna biasa, akses sesuai permission |

#### User.admin\_type

| Nilai | Deskripsi |
| - | - |
| `owner` | Owner aplikasi, full access ke semua fitur aplikasi |
| `basic` | Admin basic, hanya bisa transaksi produk digital |

#### User.admin\_tier

| Nilai | Deskripsi |
| - | - |
| `none` | Bukan admin, tidak ada hak company management |
| `business` | Tier business, bisa manage company |
| `advanced` | Tier advanced, hak manage lebih luas |
| `enterprise` | Tier enterprise, hak manage penuh |

#### User.membership\_duration\_type

| Nilai | Deskripsi |
| - | - |
| `monthly` | Langganan bulanan |
| `yearly` | Langganan tahunan |
| `custom` | Durasi kustom |
| `lifetime` | Seumur hidup |

#### WorkspaceMember.role

| Nilai | Deskripsi |
| - | - |
| `owner` | Pemilik workspace, bisa ada multiple owners |
| `admin` | Administrator workspace |
| `member` | Anggota biasa |
| `viewer` | Hanya bisa melihat, tidak bisa mengedit |

#### Workspace.settings.default\_task\_priority

| Nilai | Deskripsi |
| - | - |
| `low` | Prioritas rendah |
| `medium` | Prioritas sedang (default) |
| `high` | Prioritas tinggi |
| `urgent` | Prioritas mendesak |

#### Subscription.billing\_cycle

| Nilai | Deskripsi |
| - | - |
| `weekly` | Penagihan mingguan |
| `monthly` | Penagihan bulanan |
| `quarterly` | Penagihan per kuartal (3 bulan) |
| `yearly` | Penagihan tahunan |

#### Subscription.status

| Nilai | Deskripsi |
| - | - |
| `active` | Langganan aktif, layanan penuh |
| `paused` | Langganan ditangguhkan sementara |
| `cancelled` | Langganan dibatalkan |

#### Company.settings.batch\_allocation\_strategy

| Nilai | Deskripsi |
| - | - |
| `fifo` | First-In-First-Out: batch paling awal diterima dipakai pertama |
| `fefo` | First-Expiry-First-Out: batch paling cepat kedaluwarsa dipakai pertama |

#### Company.settings.tax.mode

| Nilai | Deskripsi |
| - | - |
| `inclusive` | Pajak sudah termasuk dalam harga |
| `exclusive` | Pajak ditambahkan ke harga |

#### Company.settings.tax.rounding

| Nilai | Deskripsi |
| - | - |
| `per_line` | Pembulatan per baris item |
| `per_transaction` | Pembulatan per total transaksi |

#### Company.settings.spoilage\_default\_treatment

| Nilai | Deskripsi |
| - | - |
| `absorbed_normal` | Spoilage diserap ke HPP unit baik (normal) |
| `expense_abnormal` | Spoilage dipisah sebagai beban periode (abnormal) |

***

### Tabel RBAC: Hak Akses CompanyMember

Tabel berikut merinci hak akses (permissions) yang dapat di-set per CompanyMember. Default nilai menunjukkan permission untuk role `employee`.

| Permission | Tipe | Default | Deskripsi |
| - | - | - | - |
| `can_view_dashboard` | Boolean | `true` | Melihat dashboard company |
| `can_view_tasks` | Boolean | `true` | Melihat daftar task |
| `can_create_tasks` | Boolean | `true` | Membuat task baru |
| `can_edit_tasks` | Boolean | `true` | Mengedit task yang ada |
| `can_delete_tasks` | Boolean | `false` | Menghapus task |
| `can_view_notes` | Boolean | `true` | melihat catatan |
| `can_create_notes` | Boolean | `true` | Membuat catatan baru |
| `can_edit_notes` | Boolean | `true` | Mengedit catatan |
| `can_delete_notes` | Boolean | `false` | Menghapus catatan |
| `can_view_hr` | Boolean | `false` | Melihat modul HR (karyawan, absensi, cuti) |
| `can_edit_hr` | Boolean | `false` | Mengedit data HR |
| `can_view_finance` | Boolean | `false` | Melihat modul keuangan (transaksi, laporan) |
| `can_edit_finance` | Boolean | `false` | Mengedit data keuangan |
| `can_view_inventory` | Boolean | `false` | Melihat modul inventaris/gudang |
| `can_edit_inventory` | Boolean | `false` | Mengedit data inventaris |
| `can_view_projects` | Boolean | `false` | Melihat modul proyek |
| `can_edit_projects` | Boolean | `false` | Mengedit data proyek |
| `can_view_pos` | Boolean | `false` | Melihat modul Point of Sale |
| `can_use_pos` | Boolean | `false` | Menggunakan/melakukan transaksi POS |
| `can_view_reports` | Boolean | `false` | Melihat laporan-laporan |
| `can_manage_members` | Boolean | `false` | Mengelola anggota company (tambah/hapus/ubah) |
| `can_manage_roles` | Boolean | `false` | Mengelola role dan permission |
| `can_view_settings` | Boolean | `false` | Melihat pengaturan company |
| `can_edit_settings` | Boolean | `false` | Mengedit pengaturan company |
| `can_manage_cashier_shift` | Boolean | `false` | Mengelola shift kasir POS |
| `can_approve_stock_opname` | Boolean | `false` | Menyetujui hasil stock opname |
| `can_count_stock_opname` | Boolean | `false` | Melakukan penghitungan stock opname |
| `can_transfer_inventory` | Boolean | `false` | Melakukan transfer antar gudang/outlet |
| `can_create_production_batch` | Boolean | `false` | Membuat batch produksi |
| `can_release_production_qc` | Boolean | `false` | Melepaskan hasil QC produksi |
| `can_view_hpp` | Boolean | `false` | Melihat perhitungan HPP (Harga Pokok Produksi) |
| `can_manage_channel_pricing` | Boolean | `false` | Mengelola harga per channel penjualan |
| `can_view_distribution` | Boolean | `false` | Melihat modul distribusi |
| `can_create_distribution_shipment` | Boolean | `false` | Membuat pengiriman distribusi |
| `can_confirm_distribution_shipment` | Boolean | `false` | Mengkonfirmasi pengiriman distribusi |
| `can_view_b2b_invoices` | Boolean | `false` | Melihat invoice B2B |
| `can_create_b2b_invoice` | Boolean | `false` | Membuat invoice B2B |
| `can_verify_b2b_payment` | Boolean | `false` | Memverifikasi pembayaran B2B |

### Matriks Role vs Permission (Rekomendasi Default)

| Permission | owner | admin | supervisor | store\_admin | stock\_admin | finance\_admin | hr\_admin | employee |
| - | - | - | - | - | - | - | - | - |
| `can_view_dashboard` | Ya | Ya | Ya | Ya | Ya | Ya | Ya | Ya |
| `can_view_tasks` | Ya | Ya | Ya | Ya | Ya | Ya | Ya | Ya |
| `can_create_tasks` | Ya | Ya | Ya | Ya | Ya | Ya | Ya | Ya |
| `can_edit_tasks` | Ya | Ya | Ya | Ya | Ya | Ya | Ya | Ya |
| `can_delete_tasks` | Ya | Ya | Ya | Tidak | Tidak | Tidak | Tidak | Tidak |
| `can_view_hr` | Ya | Ya | Ya | Tidak | Tidak | Tidak | Ya | Tidak |
| `can_edit_hr` | Ya | Ya | Tidak | Tidak | Tidak | Tidak | Ya | Tidak |
| `can_view_finance` | Ya | Ya | Tidak | Tidak | Tidak | Ya | Tidak | Tidak |
| `can_edit_finance` | Ya | Ya | Tidak | Tidak | Tidak | Ya | Tidak | Tidak |
| `can_view_inventory` | Ya | Ya | Ya | Ya | Ya | Tidak | Tidak | Tidak |
| `can_edit_inventory` | Ya | Ya | Ya | Ya | Ya | Tidak | Tidak | Tidak |
| `can_view_pos` | Ya | Ya | Ya | Ya | Tidak | Tidak | Tidak | Tidak |
| `can_use_pos` | Ya | Ya | Ya | Ya | Tidak | Tidak | Tidak | Tidak |
| `can_view_reports` | Ya | Ya | Ya | Ya | Ya | Ya | Ya | Tidak |
| `can_manage_members` | Ya | Ya | Tidak | Tidak | Tidak | Tidak | Tidak | Tidak |
| `can_manage_roles` | Ya | Ya | Tidak | Tidak | Tidak | Tidak | Tidak | Tidak |
| `can_view_settings` | Ya | Ya | Tidak | Tidak | Tidak | Tidak | Tidak | Tidak |
| `can_edit_settings` | Ya | Tidak | Tidak | Tidak | Tidak | Tidak | Tidak | Tidak |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.