# CYDM Technical Architecture

## Stack Decision Matrix

| Layer | Technology | Rationale |
|-------|------------|-----------|
| **Backend** | Laravel 12 + PHP 8.3 | LTS, strong ecosystem, Tanzanian dev talent pool |
| **Frontend** | Inertia.js + React 19 + TypeScript | SPA feel, Laravel integration, type safety |
| **UI Library** | SHAD UI (shadcn/ui) | Headless, accessible, customizable, Tailwind-native |
| **Styling** | Tailwind CSS v4 + CSS Variables | Design tokens, per-tenant theming, dark mode |
| **Database** | PostgreSQL 16 | JSONB for config, robust, ACID, BOT audit requirements |
| **Multi-tenancy** | Stancl/Tenancy v4 (single DB) | Row-level isolation, subdomain routing, 5000+ tenant scale |
| **Queue/Jobs** | Redis + Horizon | Tenant-aware jobs, monitoring, retry logic |
| **Cache** | Redis (Valkey) | Sessions, rate limits, tenant-prefixed keys |
| **Storage** | S3-compatible (MinIO/Wasabi) | Tenant-prefixed paths, encrypted, versioned |
| **Search** | Meilisearch + Scout | Fast member/loan search, multi-tenant indexes |
| **Payments** | Stripe (subscriptions) + Custom Gateway Adapters | 13+ Tanzanian providers (M-Pesa, Airtel, Mixx, HaloPesa, TIPS) |
| **Auth** | Laravel Sanctum + 2FA (TOTP/SMS) | SPA tokens, mobile API, regulatory compliance |
| **Permissions** | Spatie Laravel Permission (team-scoped) | Per-tenant RBAC, roles don't leak |
| **Testing** | Pest (PHP) + Vitest (React) + Playwright (E2E) | Parallel, fast, tenant isolation tests |
| **Observability** | Sentry + Laravel Pulse + Prometheus/Grafana | Errors, performance, custom metrics |
| **CI/CD** | GitHub Actions → Docker → K8s/VPS | Zero-downtime, per-tenant migration orchestration |

## Multi-Tenancy Architecture (Single Database, Row-Level)

```
┌─────────────────────────────────────────────────────────────┐
│                    CENTRAL APPLICATION                       │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   Tenant    │  │   Domain    │  │   Subscription      │  │
│  │  Registry   │  │  Mapping    │  │   & Billing         │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
        ┌──────────┐    ┌──────────┐    ┌──────────┐
        │ Tenant A │    │ Tenant B │    │ Tenant N │
        │ *.cydm.. │    │ *.cydm.. │    │ custom.. │
        └──────────┘    └──────────┘    └──────────┘
              │               │               │
              └───────────────┼───────────────┘
                              ▼
        ┌─────────────────────────────────────┐
        │     SHARED POSTGRESQL DATABASE       │
        │  • All tables have tenant_id (FK)   │
        │  • Global scopes enforce isolation  │
        │  • Composite indexes (tenant_id, *) │
        │  • RLS policies for defense-in-depth│
        └─────────────────────────────────────┘
```

### Key Implementation Decisions

1. **Single Database + tenant_id** (not DB-per-tenant)
   - Scales to 5000+ tenants on managed Postgres
   - Cross-tenant analytics trivial (super-admin)
   - Single migration run, simpler backups
   - Regulatory: Data residency met via encryption + audit logs

2. **Subdomain Routing** (`tenant.cydm.co.tz`)
   - Wildcard DNS + SSL (Let's Encrypt / Caddy)
   - Middleware resolves tenant → binds to container
   - Session cookies: host-only (no SESSION_DOMAIN)

3. **Global Scopes on ALL Tenant Models**
   ```php
   trait BelongsToTenant {
       protected static function bootBelongsToTenant() {
           static::addGlobalScope('tenant', fn($q) => 
               $q->where('tenant_id', Tenant::current()->id)
           );
           static::creating(fn($m) => 
               $m->tenant_id ??= Tenant::current()->id
           );
       }
   }
   ```
   - CI check: `grep -r "withoutGlobalScope" --include="*.php"`

4. **Tenant-Aware Queues**
   - Every job implements `TenantAware` interface
   - Job middleware re-resolves tenant from payload
   - Horizon queues prefixed: `tenant:{id}:default`

5. **Control Plane vs Tenant Plane Models**
   - `GlobalModel` trait = intentionally unscoped (plans, countries, currencies)
   - All business models use `BelongsToTenant`

---

## SHAD UI Integration with Laravel

### Installation & Configuration
```bash
# 1. Fresh Laravel 12 with React starter
laravel new cydm --react --typescript

# 2. Install SHAD UI
cd cydm
pnpm dlx shadcn@latest init --template laravel

# 3. Configure for CSS variables (required for theming)
# components.json → "tailwind": { "cssVariables": true }
```

### Custom Blue/Orange Theme (CSS Variables)
```css
/* resources/css/app.css */
@import "tailwindcss";
@import "shadcn/tailwind.css";

@custom-variant dark (&:is(.dark *));

@theme inline {
  /* Radius tokens */
  --radius-sm: calc(var(--radius) * 0.6);
  --radius-md: calc(var(--radius) * 0.8);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) * 1.4);
  
  /* CYDM Brand Colors - Blue Primary, Orange Accent */
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-ring: var(--ring);
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  
  /* PRIMARY: Professional Blue (trust, finance) */
  --primary: oklch(0.45 0.22 258);      /* ~#2563EB - blue-600 */
  --primary-foreground: oklch(0.985 0 0);
  
  /* ACCENT: Warm Orange (energy, accessibility) */
  --accent: oklch(0.7 0.18 45);         /* ~#EA580C - orange-600 */
  --accent-foreground: oklch(0.985 0 0);
  
  /* Semantic tokens */
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.45 0.22 258);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --primary: oklch(0.65 0.22 258);      /* Lighter blue for dark mode */
  --primary-foreground: oklch(0.145 0 0);
  --accent: oklch(0.75 0.18 45);        /* Brighter orange for dark mode */
  --accent-foreground: oklch(0.145 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.65 0.22 258);
}

@layer base {
  * { @apply border-border outline-ring/50; }
  body { @apply bg-background text-foreground; }
}
```

### Per-Tenant Theme Override (Runtime)
```typescript
// resources/js/hooks/useTenantTheme.ts
export function useTenantTheme() {
  const { tenant } = useTenant(); // Inertia shared prop
  
  useEffect(() => {
    if (!tenant?.branding) return;
    
    const root = document.documentElement;
    const { primary_color, accent_color, border_radius } = tenant.branding;
    
    // Validate colors are blue/orange family only
    if (isValidBrandColor(primary_color, 'blue')) {
      root.style.setProperty('--primary', primary_color);
      root.style.setProperty('--ring', primary_color);
    }
    if (isValidBrandColor(accent_color, 'orange')) {
      root.style.setProperty('--accent', accent_color);
    }
    if (border_radius) {
      root.style.setProperty('--radius', `${border_radius}rem`);
    }
  }, [tenant?.branding]);
}
```

### Component Generation Strategy
```bash
# Core components (add once, customize per tenant via props)
pnpm dlx shadcn@latest add button input select dialog table \
  tabs toast dropdown-menu avatar badge card separator \
  sheet tooltip popover hover-card radio-group checkbox \
  switch slider progress skeleton calendar date-picker \
  command pagination data-table chart-area chart-bar \
  chart-line chart-pie chart-radar chart-scatter
```

---

## Database Schema Highlights (Tenant-Scoped Tables)

```sql
-- Core tenant table (central)
CREATE TABLE tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,           -- subdomain
    custom_domain VARCHAR(255) UNIQUE,           -- optional CNAME
    license_number VARCHAR(100),                 -- BOT/TCDC license
    license_type ENUM('category_a','category_b','tier_2','tier_4'),
    plan ENUM('starter','growth','enterprise') DEFAULT 'starter',
    status ENUM('trial','active','suspended','deleted') DEFAULT 'trial',
    trial_ends_at TIMESTAMP,
    subscription_ends_at TIMESTAMP,
    stripe_customer_id VARCHAR(255),
    branding JSONB,                              -- logo, colors, strings
    settings JSONB,                              -- numbering, fees, workflows
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- All tenant tables have tenant_id + composite indexes
CREATE TABLE members (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    member_number VARCHAR(50) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    middle_name VARCHAR(100),
    last_name VARCHAR(100) NOT NULL,
    nida_number VARCHAR(20) UNIQUE,              -- encrypted at rest
    tin_number VARCHAR(20),
    phone VARCHAR(20), email VARCHAR(255),
    date_of_birth DATE, gender ENUM('M','F','O'),
    marital_status ENUM('single','married','divorced','widowed'),
    occupation VARCHAR(100), monthly_income DECIMAL(15,2),
    address TEXT, ward VARCHAR(100), district VARCHAR(100), region VARCHAR(100),
    kyc_status ENUM('pending','verified','rejected') DEFAULT 'pending',
    kyc_verified_at TIMESTAMP, kyc_verified_by UUID,
    status ENUM('active','dormant','suspended','exited') DEFAULT 'active',
    joined_at DATE DEFAULT CURRENT_DATE,
    exited_at DATE, exit_reason TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    CONSTRAINT uq_tenant_member_number UNIQUE (tenant_id, member_number)
);

CREATE INDEX idx_members_tenant_status ON members(tenant_id, status);
CREATE INDEX idx_members_tenant_nida ON members(tenant_id, nida_number);
```

---

## Folder Structure (Modular Monolith)

```
cydm/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Admin/           # Super-admin (central)
│   │   │   ├── API/V1/          # Tenant API (Sanctum)
│   │   │   ├── Auth/            # Login, 2FA, password reset
│   │   │   ├── Billing/         # Stripe webhooks, subscriptions
│   │   │   ├── Members/         # Member CRUD, KYC, groups
│   │   │   ├── Loans/           # Applications, schedules, collections
│   │   │   ├── Savings/         # Deposits, withdrawals, interest
│   │   │   ├── Shares/          # Share capital, dividends
│   │   │   ├── Accounting/      # GL, journals, reports, assets
│   │   │   ├── Reporting/       # BOT MSP, custom reports
│   │   │   ├── Payments/        # Mobile money gateways
│   │   │   ├── Communications/  # SMS, WhatsApp, email
│   │   │   ├── Settings/        # Tenant config, branding
│   │   │   └── Tenants/         # Tenant onboarding, impersonation
│   │   └── Middleware/
│   │       ├── InitializeTenancy.php
│   │       ├── TenantScopeBindings.php
│   │       └── EnsureTenantAccess.php
│   ├── Models/
│   │   ├── Concerns/
│   │   │   └── BelongsToTenant.php
│   │   ├── Tenant.php
│   │   ├── Member.php
│   │   ├── Loan.php
│   │   ├── SavingsAccount.php
│   │   ├── ShareAccount.php
│   │   ├── JournalEntry.php
│   │   └── ...
│   ├── Services/
│   │   ├── Tenancy/
│   │   │   ├── TenantProvisioningService.php
│   │   │   ├── TenantBrandingService.php
│   │   │   └── TenantResolver.php
│   │   ├── Loans/
│   │   │   ├── LoanCalculator.php
│   │   │   ├── LoanWorkflowService.php
│   │   │   └── RepaymentAllocator.php
│   │   ├── Accounting/
│   │   │   ├── PostingEngine.php
│   │   │   └── FinancialStatementGenerator.php
│   │   ├── Payments/
│   │   │   ├── GatewayRegistry.php
│   │   │   ├── Drivers/MpesaDriver.php, AirtelDriver.php...
│   │   │   └── ReconciliationService.php
│   │   ├── Reporting/
│   │   │   ├── MSPReportGenerator.php
│   │   │   └── CRBSubmissionService.php
│   │   └── Notifications/
│   │       ├── SMSGateway.php
│   │       └── WhatsAppGateway.php
│   ├── Jobs/
│   │   ├── TenantAwareJob.php (interface)
│   │   ├── ProvisionTenantJob.php
│   │   ├── GenerateMSPReportsJob.php
│   │   ├── SendPaymentRemindersJob.php
│   │   └── SubmitCRBDataJob.php
│   ├── Observers/               # Audit logging, side effects
│   ├── Policies/                # Per-tenant authorization
│   └── Support/
│       └── Money.php            # Integer cents, TZS currency
├── resources/
│   ├── js/
│   │   ├── Components/
│   │   │   ├── ui/              # SHAD UI components (generated)
│   │   │   ├── forms/           # Reusable form components
│   │   │   ├── tables/          # DataTable, ColumnDef
│   │   │   ├── charts/          # Recharts wrappers
│   │   │   └── layout/          # Sidebar, Header, Breadcrumbs
│   │   ├── Pages/
│   │   │   ├── Admin/           # Super-admin pages
│   │   │   ├── Dashboard/       # Role-based dashboards
│   │   │   ├── Members/         # Member management
│   │   │   ├── Loans/           # Loan pipeline, details
│   │   │   ├── Savings/         # Savings products, transactions
│   │   │   ├── Accounting/      # GL, trial balance, reports
│   │   │   ├── Reports/         # BOT MSP, custom builder
│   │   │   ├── Settings/        # Tenant config, branding
│   │   │   └── Auth/            # Login, 2FA, register
│   │   ├── hooks/
│   │   │   ├── useTenant.ts
│   │   │   ├── useTenantTheme.ts
│   │   │   └── usePermissions.ts
│   │   ├── lib/
│   │   │   ├── utils.ts         # cn(), formatCurrency(), etc.
│   │   │   ├── api.ts           # Typed API client
│   │   │   └── validations.ts   # Zod schemas
│   │   └── types/               # TypeScript interfaces
│   └── css/app.css              # Theme CSS variables
├── database/
│   ├── migrations/
│   │   ├── central/             # Tenants, domains, plans, users
│   │   └── tenant/              # All business tables (scoped)
│   └── seeders/
├── routes/
│   ├── web.php                  # Central + tenant routes
│   ├── api.php                  # Tenant API (Sanctum)
│   └── admin.php                # Super-admin only
├── config/
│   ├── tenancy.php              # Stancl config
│   ├── permissions.php          # Spatie config
│   ├── cashier.php              # Stripe config
│   └── shadcn.php               # Component registry
└── tests/
    ├── Feature/
    │   ├── Tenancy/             # Isolation, provisioning, domains
    │   ├── Loans/               # Workflow, calculations, PAR
    │   ├── Accounting/          # Double-entry, balances
    │   └── Reporting/           # MSP forms, CRB
    ├── Unit/
    │   ├── Services/
    │   └── Models/
    └── Browser/                 # Playwright E2E
```

---

## Deployment Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                        PRODUCTION                                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
│  │   Cloudflare │  │   Load       │  │   Kubernetes/GKE     │  │
│  │   (WAF, CDN, │──▶│  Balancer    │──▶│  ┌────────────────┐  │  │
│  │   DNS, SSL)  │  │  (Caddy/NGINX)│  │  │ Laravel Octane  │  │  │
│  └──────────────┘  └──────────────┘  │  │  (FrankenPHP)   │  │  │
│                                      │  │  ├─ Web Workers  │  │  │
│                                      │  │  ├─ Horizon      │  │  │
│                                      │  │  └─ Scheduler    │  │  │
│                                      │  └────────────────┘  │  │
│                                      └──────────────────────┘  │
│                                             │                   │
│                    ┌────────────────────────┼───────────────┐  │
│                    ▼                        ▼               ▼  │
│             ┌─────────────┐         ┌─────────────┐  ┌─────────────┐
│             │ PostgreSQL  │         │    Redis    │  │   MinIO     │
│             │  (Managed)  │         │  (Valkey)   │  │   (S3)      │
│             │  - Primary  │         │  - Sessions │  │  - Tenant   │
│             │  - Replica  │         │  - Cache    │  │    prefixes │
│             │  - PITR     │         │  - Queues   │  │  - Versioned│
│             └─────────────┘         └─────────────┘  └─────────────┘
└─────────────────────────────────────────────────────────────────┘
```

---

## Security & Compliance Checklist

- [ ] **Data Encryption**: AES-255 at rest, TLS 1.3 in transit
- [ ] **PII Protection**: Field-level encryption (NIDA, phone, address)
- [ ] **Audit Logging**: Immutable, all CRUD + auth events
- [ ] **2FA**: TOTP + SMS OTP mandatory for staff
- [ ] **Session Security**: Host-only cookies, CSRF, secure flags
- [ ] **Rate Limiting**: Per-tenant, per-IP, per-endpoint
- [ ] **SQL Injection**: Eloquent only, no raw queries without review
- [ ] **XSS Protection**: React auto-escape, CSP headers
- [ ] **PDPA 2022**: Data minimization, consent, right to deletion
- [ ] **AML/CFT**: Transaction monitoring, SAR filing, sanctions screening
- [ ] **PCI-DSS**: If card payments (SAQ-A via Stripe Elements)
- [ ] **Backup**: Daily PITR, per-tenant export on demand
- [ ] **DR**: RPO < 1hr, RTO < 4hr, tested quarterly