# Backend Implementatie — HSEQ SaaS Multi-Tenant

| Veld        | Waarde                                        |
|-------------|-----------------------------------------------|
| **Project** | 2026-hseq-saas-multitenant                    |
| **Type**    | Backend Implementatie Documentatie            |
| **Auteur**  | Backend Developer Agent                       |
| **Versie**  | 1.0                                           |
| **Datum**   | 2026-05-26                                    |
| **Status**  | Sandbox — architectuur-implementatie          |

---

## 1. Samenvatting

Dit document beschrijft de backend-implementatie van de multi-tenant architectuur voor het HSEQ SaaS platform. De implementatie is gebaseerd op de Phase 2 database-architectuur [1] en systeemarchitectuur [2], en omvat vijf kerncomponenten:

| Component | Bestand | Verantwoordelijkheid |
|-----------|---------|---------------------|
| ORM Modellen | `models_v1.0.py` | SQLAlchemy modellen met relatie-definities |
| Company Manager | `company_manager_v1.0.py` | CRUD operaties voor tenant-beheer |
| Module Manager | `module_manager_v1.0.py` | Module toggle management per tenant |
| Tenant Middleware | `tenant_middleware_v1.0.py` | Tenant-resolutie en RLS context |
| API Routes | `api_routes_v1.0.py` | REST endpoints voor tenant management |

**Technologie stack:** Python 3.11+, Flask, SQLAlchemy 2.x, PostgreSQL 16+.

---

## 2. Modellen (`models_v1.0.py`)

### 2.1 Overzicht

| Model | Tabel | PK Type | Relaties |
|-------|-------|---------|----------|
| `Company` | `companies` | UUID | → CompanyUser (1:N), → CompanyModule (1:N) |
| `CompanyUser` | `company_users` | UUID | → Company (N:1) |
| `CompanyModule` | `company_modules` | UUID | → Company (N:1) |
| `CompanyPermission` | `company_permissions` | UUID | → Company (N:1, nullable) |
| `ModuleCatalog` | `module_catalog` | UUID | Geen FK-relaties |

### 2.2 TenantMixin

`TenantMixin` is een herbruikbare mixin die `company_id` toevoegt aan tenant-scoped modellen. Alle toekomstige tenant-tabellen (incidents, employees, etc.) erven van deze mixin.

```python
class TenantMixin:
    company_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("companies.id", ondelete="CASCADE"),
        nullable=False, index=True,
    )
```

### 2.3 Company Model

Kernvelden:

| Veld | Type | Constraint | Doel |
|------|------|------------|------|
| `id` | UUID | PK, auto-gen | Unieke tenant-identificatie |
| `slug` | String(100) | UNIQUE, NOT NULL | URL-safe identifier |
| `name` | String(200) | NOT NULL | Bedrijfsnaam |
| `plan` | String(50) | NOT NULL, default 'starter' | Abonnementstype |
| `status` | String(20) | NOT NULL, default 'active' | Bedrijfsstatus |
| `settings` | JSONB | NOT NULL, default {} | Flexibele configuratie |
| `brzo_tier` | String(20) | nullable | Seveso-classificatie |

UUID primary keys voorkomen orde-lek tussen tenants bij auto-increment [1].

### 2.4 Plan-Module Mapping

Constanten voor standaard module-toewijzing per abonnement:

| Plan | Modules |
|------|---------|
| starter | compliance, incidents, risk_assessment, ptw, moc, training (6) |
| professional | starter + environment, brzo, intelligence (9) |
| enterprise | professional + agents, knowledge (11) |

### 2.5 Validatieconstanten

```python
VALID_ROLES = ("owner", "admin", "manager", "user", "viewer")
VALID_PLANS = ("starter", "professional", "enterprise")
VALID_STATUSES = ("active", "suspended", "churned", "trial")
VALID_BRZO_TIERS = ("lower", "upper", "none")
VALID_PERMISSION_ACTIONS = ("create", "read", "update", "delete", "export", "approve")
```

---

## 3. Company Manager (`company_manager_v1.0.py`)

### 3.1 Functies

| Functie | Operatie | Validaite |
|---------|----------|-----------|
| `create_company()` | INSERT + standaardmodules | plan, brzo_tier, slug-uniqueness |
| `get_company()` | SELECT by UUID | — |
| `get_company_by_slug()` | SELECT by slug | — |
| `list_companies()` | SELECT met filters | status, plan |
| `update_company()` | UPDATE (partial) | plan, status, brzo_tier |
| `delete_company()` | Soft delete (status='churned') | — |

### 3.2 Slug Generatie

De `generate_slug()` functie converteert bedrijfsnamen naar URL-safe identifiers:

- Input: `"Acme Chemie B.V."` → Output: `"acme-chemie-bv"`
- Bij conflict wordt een numeriek suffix toegevoegd: `"acme-chemie-bv-1"`

### 3.3 Company Aanmaak Flow

```
create_company(session, name="Acme Chemie", contact_email="info@acme.nl", plan="professional")
  ├── Slug generatie: "acme-chemie"
  ├── Validatie: plan ∈ VALID_PLANS, brzo_tier ∈ VALID_BRZO_TIERS
  ├── Company record INSERT
  ├── Flush (zodat company.id beschikbaar is)
  └── PLAN_DEFAULT_MODULES["professional"] → 9 CompanyModule records INSERT
```

### 3.4 Soft Delete

Hard delete wordt bewust NIET ondersteund i.v.m.:
- BRZO-bewaarplicht (minimaal 5 jaar) [1]
- Audit-trail integriteit
- Mogelijke heractivering bij terugkerende klanten

`delete_company()` zet `status = 'churned'` en retourneert `True`.

### 3.5 Plan Upgrade/Downgrade

`update_company()` met `plan` parameter triggert `_sync_modules_to_plan()`:
- Modules buiten het nieuwe plan → `enabled = False`
- Nieuwe modules binnen het plan → `CompanyModule` records aangemaakt

---

## 4. Module Manager (`module_manager_v1.0.py`)

### 4.1 Functies

| Functie | Operatie | Bijzonderheid |
|---------|----------|---------------|
| `get_enabled_modules()` | SELECT (enabled=True) | Lijst van actieve module keys |
| `get_all_modules_status()` | SELECT (all) | Status van alle modules |
| `is_module_enabled()` | SELECT single | Runtime check voor decorators |
| `enable_module()` | INSERT of UPDATE | Valideert tegen plan |
| `disable_module()` | UPDATE (soft) | Module-record behouden |
| `update_module_config()` | UPDATE (JSONB merge) | Merge met bestaande config |
| `bulk_set_modules()` | Bulk INSERT/UPDATE | Onboarding + plan-wijzigingen |

### 4.2 Plan Validatie

`enable_module()` valideert dat de gevraagde module beschikbaar is voor het huidige abonnement. Bij een ongeldige combinatie:

```python
raise ValueError(
    f"Module 'agents' is niet beschikbaar voor plan 'starter'. "
    f"Upgrade naar een hoger abonnement voor toegang."
)
```

### 4.3 Configuratie Merge

`update_module_config()` merge de nieuwe configuratie met bestaande instellingen:

```python
existing_config.update(config)  # dict merge
module.config = existing_config
```

Dit voorkomt dat ongespecificeerde velden verloren gaan bij een gedeeltelijke update.

---

## 5. Tenant Middleware (`tenant_middleware_v1.0.py`)

### 5.1 TenantContext

`TenantContext` is het centrale object dat per request beschikbaar is via `g.tenant`:

```python
class TenantContext:
    company_id: uuid.UUID
    company_name: str
    slug: str
    plan: str
    status: str
    settings: dict
    modules: set[str]
    role: str
```

Methoden:
- `is_module_enabled(key)` → bool
- `to_dict()` → dict (voor API-responses)

### 5.2 Middleware Flow

```
HTTP Request
  → before_request: _resolve_tenant()
      ├── Endpoint whitelist check (auth, health, static)
      ├── company_id extractie (session → header → slug)
      ├── Company lookup + status validatie
      │   ├── suspended → 403 Forbidden
      │   └── churned → 403 Forbidden
      ├── Modules laden → g.tenant.modules
      └── PostgreSQL RLS: SET app.company_id = :cid
  → Route Handler (met g.tenant beschikbaar)
  → teardown_request: commit/rollback + session close
```

### 5.3 Company ID Extractie Prioriteit

| Prioriteit | Bron | Use Case |
|:---:|------|----------|
| 1 | `session['company_id']` | Browser flow (session-based auth) |
| 2 | `X-Company-Id` header | API flow (programmatic access) |
| 3 | `X-Tenant-Slug` header | Alternatieve API flow |

### 5.4 Decorators

| Decorator | Parameters | Gedrag bij Falen |
|-----------|-----------|------------------|
| `@require_tenant` | — | 401 Unauthorized |
| `@require_module(key)` | module_key | 403 Forbidden + upgrade-bericht |
| `@require_role(*roles)` | rolnamen | 403 Forbidden + vereiste rollen |

Voorbeeld gebruik:

```python
@app.route('/api/v2/incidents/dashboard')
@require_tenant
@require_module('incidents')
def incidents_dashboard():
    tenant = g.tenant
    # queries automatisch gefilterd via RLS
```

### 5.5 RLS Integratie

Na succesvolle tenant-resolutie stelt de middleware de PostgreSQL sessie-variabele in:

```sql
SET app.company_id = '550e8400-e29b-41d4-a716-446655440000';
```

Alle RLS policies op tenant-tabellen filteren automatisch op deze waarde [1]. Bij applicatiefouten (vergeten filter) blokkeert PostgreSQL toegang tot andere tenants — defense-in-depth.

---

## 6. API Routes (`api_routes_v1.0.py`)

### 6.1 Blueprint Structuur

| Blueprint | URL Prefix | Doel |
|-----------|-----------|------|
| `tenant_bp` | `/api/v2/tenant/` | Tenant CRUD + configuratie |
| `module_bp` | `/api/v2/modules/` | Module toggle management |
| `admin_bp` | `/api/v2/admin/` | User & permission management |

### 6.2 Tenant Endpoints

| Methode | Endpoint | Permissie | Beschrijving |
|---------|----------|-----------|-------------|
| GET | `/api/v2/tenant/` | Ingelogd | Huidige tenant-context |
| GET | `/api/v2/tenant/companies` | owner, admin | Lijst bedrijven |
| POST | `/api/v2/tenant/companies` | owner | Nieuw bedrijf aanmaken |
| GET | `/api/v2/tenant/companies/<id>` | owner, admin | Bedrijfsgegevens |
| PUT | `/api/v2/tenant/companies/<id>` | owner, admin | Bedrijf bijwerken |
| DELETE | `/api/v2/tenant/companies/<id>` | owner | Bedrijf deactiveren |

### 6.3 Module Endpoints

| Methode | Endpoint | Permissie | Beschrijving |
|---------|----------|-----------|-------------|
| GET | `/api/v2/modules/` | Ingelogd | Alle modules status |
| GET | `/api/v2/modules/<key>` | Ingelogd | Specifieke module status |
| POST | `/api/v2/modules/<key>/enable` | owner, admin | Module inschakelen |
| POST | `/api/v2/modules/<key>/disable` | owner, admin | Module uitschakelen |
| PUT | `/api/v2/modules/<key>/config` | owner, admin | Config bijwerken |
| POST | `/api/v2/modules/bulk` | owner, admin | Bulk modules instellen |

### 6.4 Admin Endpoints

| Methode | Endpoint | Permissie | Beschrijving |
|---------|----------|-----------|-------------|
| GET | `/api/v2/admin/users` | owner, admin | Gebruikerslijst |
| POST | `/api/v2/admin/users` | owner, admin | Gebruiker toevoegen |
| PUT | `/api/v2/admin/users/<id>` | owner, admin | Gebruiker bijwerken |
| DELETE | `/api/v2/admin/users/<id>` | owner | Gebruiker verwijderen |
| GET | `/api/v2/admin/permissions` | owner, admin | Permissies bekijken |

### 6.5 Response Formaat

Alle responses volgen een consistent formaat:

```json
{
  "status": "success",
  "data": { }
}
```

Foutresponses:

```json
{
  "error": "Forbidden",
  "message": "Module 'agents' is niet beschikbaar voor uw abonnement (starter)."
}
```

HTTP statuscodes: 200 (OK), 201 (Created), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found).

---

## 7. Bestanden Overzicht

| Bestand | Regels | Doel |
|---------|--------|------|
| `models_v1.0.py` | ~310 | SQLAlchemy ORM modellen + constanten |
| `company_manager_v1.0.py` | ~260 | CRUD operaties voor companies |
| `module_manager_v1.0.py` | ~230 | Module toggle management |
| `tenant_middleware_v1.0.py` | ~260 | Middleware + decorators |
| `api_routes_v1.0.py` | ~510 | REST API endpoints |
| **Totaal** | **~1.570** | |

---

## 8. Design Keuzes

### 8.1 UUID vs Integer PKs

UUID primary keys voorkomen orde-lek: bij auto-increment kan een aanvaller het aantal tenants schatten door sequentiële IDs te observeren [1]. UUIDs zijn niet-raadbaar en universeel uniek.

### 8.2 Soft Delete

Bedrijven worden nooit fysiek verwijderd (hard delete). Reden:
- BRZO-bewaarplicht vereist minimaal 5 jaar bewaring [1]
- Audit-trail integriteit
- Heractiveringsmogelijkheid bij terugkerende klanten

### 8.3 JSONB voor Settings

`settings` en `config` velden gebruiken PostgreSQL JSONB voor:
- Flexibele per-tenant configuratie zonder schema-wijzigingen
- Efficiënte querying op JSON-velden (GIN index support)
- Toekomstbestendig: nieuwe instellingen zonder migratie

### 8.4 Plan-Gated Modules

Module-toegang is gekoppeld aan het abonnement (plan). Bij upgrade/downgrade:
- Nieuwe modules worden automatisch aangemaakt en ingeschakeld
- Modules buiten het plan worden uitgeschakeld (niet verwijderd)
- Configuratie blijft behouden bij heractivering

---

## 9. Gebruik Voorbeeld

### 9.1 Flask App Setup

```python
from flask import Flask
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from models_v1_0 import Base
from tenant_middleware_v1_0 import init_tenant_middleware
from api_routes_v1_0 import register_blueprints

app = Flask(__name__)
app.secret_key = "your-secret-key"

engine = create_engine("postgresql://user:pass@localhost/hseq_saas")
SessionLocal = sessionmaker(bind=engine)
Base.metadata.create_all(engine)

init_tenant_middleware(app, SessionLocal, rls_enabled=True)
register_blueprints(app)
```

### 9.2 Nieuw Bedrijf Aanmaken

```python
from company_manager_v1_0 import create_company

with SessionLocal() as session:
    company = create_company(
        session,
        name="Acme Chemie B.V.",
        contact_email="info@acme-chemie.nl",
        plan="professional",
        brzo_tier="upper",
        kvk_number="12345678",
    )
    session.commit()
    # company.slug → "acme-chemie-bv"
    # 9 CompanyModule records automatisch aangemaakt
```

### 9.3 Module Toggle

```python
from module_manager_v1_0 import enable_module, disable_module

with SessionLocal() as session:
    # Module inschakelen (met plan-validatie)
    enable_module(session, company.id, "intelligence", plan="professional")
    
    # Module uitschakelen
    disable_module(session, company.id, "training")
    
    session.commit()
```

---

## 10. Beveiligingsoverwegingen

### 10.1 Defense-in-Depth

| Laag | Mechanisme | Component |
|------|-----------|-----------|
| L1: Applicatie | TenantMixin, g.tenant filtering | Middleware |
| L2: Database | PostgreSQL RLS policies | Middleware (`SET app.company_id`) |
| L3: API | Decorators (@require_tenant, @require_module, @require_role) | Routes |

Bij falen van L1 (applicatiefout) blokkeert L2 (RLS) cross-tenant toegang.

### 10.2 Endpoint Bescherming

| Endpoint categorie | Minimale rol | Decorator stack |
|-------------------|-------------|-----------------|
| Lezen (GET) | Ingelogd | `@require_tenant` |
| Schrijven (POST/PUT) | owner, admin | `@require_tenant` + `@require_role` |
| Verwijderen (DELETE) | owner | `@require_tenant` + `@require_role("owner")` |
| Module-specifiek | — | `@require_tenant` + `@require_module(key)` |

### 10.3 Tenant Spoofing Preventie

- Tenant-context via session (server-side), NIET via URL-segment
- `X-Company-Id` header wordt gevalideerd tegen gebruikersrechten
- RLS als defensieve tweede laag bij header-manipulatie

---

## 11. Vereisten

### 11.1 Python Dependencies

```
flask>=3.0
sqlalchemy>=2.0
psycopg2-binary>=2.9
```

### 11.2 Database

- PostgreSQL 16+ met `uuid-ossp` of `pgcrypto` extensie
- RLS policies geconfigureerd per Phase 2 architectuur [1]
- `app.company_id` sessie-variabele ondersteund

### 11.3 Infrastructuur

- PgBouncer in transaction-mode (voor connection pooling)
- HTTPS verplicht (session security)
- Cloudflare CDN aanbevolen (WAF, rate limiting)

---

## 12. Bronverwijzingen

| #  | Bron |
|----|------|
| 1  | Phase 2 Database Architectuur — `phase2_database_architecture_v1.0.md` (2026-05-26) |
| 2  | Phase 2 Systeemarchitectuur — `phase2_system_architecture_v1.0.md` (2026-05-26) |
| 3  | PostgreSQL Row-Level Security — postgresql.org/docs/current/ddl-rowsecurity.html |
| 4  | SQLAlchemy 2.x ORM documentatie — docs.sqlalchemy.org |
| 5  | Flask blueprints en middleware patterns — flask.palletsprojects.com |
| 6  | BRZO/Seveso III richtlijn — Besluit risico's zware ongevallen 2015 |

---

## 13. Verify — TierVerify Log

| Controle-item | Resultaat | Notitie |
|---|---|---|
| Code conform Phase 2 architectuur | ✅ PASS | Modellen, middleware, routes volgen [1][2] design |
| SQLAlchemy 2.x syntax | ✅ PASS | Mapped columns, type hints, DeclarativeBase |
| PEP 8 compliant | ✅ PASS | Type hints, docstrings, 88-char line limit |
| UUID primary keys | ✅ PASS | Alle modellen gebruiken UUID PKs [1] |
| Soft delete (geen hard delete) | ✅ PASS | delete_company() → status='churned' |
| Plan-gated module validatie | ✅ PASS | enable_module() valideert tegen plan |
| RLS context integratie | ✅ PASS | Middleware stelt SET app.company_id in [3] |
| Defense-in-depth (3 lagen) | ✅ PASS | Applicatie + Database + API isolatie |
| Decorators: require_tenant, require_module, require_role | ✅ PASS | Volledig geïmplementeerd |
| Consistent JSON response formaat | ✅ PASS | status/data/error formaat |
| Geen live database connecties | ✅ PASS | Sandbox implementatie, geen execute |
| Bestanden in working/backend/ met _v1.0 tag | ✅ PASS | 5 Python bestanden + 1 documentatie |
| Bronverwijzingen compleet | ✅ PASS | 6 bronnen gedocumenteerd |
| Document-header aanwezig | ✅ PASS | Project, Type, Auteur, Versie, Datum, Status |
