# HTTP Request Flow

**Document Version:** 1.0
**Last Updated:** 2026-01-10
**Status:** Production LIVE
**Flow Type:** HTTP Request Handling & Response Cycle

---

## Overview

This document describes the **complete HTTP request flow** for the Norda Biznes Partner application, from external user through reverse proxy to Flask application and back. It covers:

- **Complete request path** (Internet → Fortigate → NPM → Flask → Response)
- **Critical port configurations** and routing decisions
- **SSL/TLS termination** and security boundaries
- **Request/response transformation** at each layer
- **Common failure scenarios** and troubleshooting

**Key Infrastructure:**
- **Public Entry:** 85.237.177.83:443 (Fortigate NAT)
- **Reverse Proxy:** NPM on 10.22.68.250:443 (SSL termination)
- **Backend Application:** Flask/Gunicorn on 10.22.68.249:5000
- **Protocol Flow:** HTTPS → NPM → HTTP → Flask → HTTP → NPM → HTTPS

**⚠️ CRITICAL CONFIGURATION:**
```
NPM MUST forward to port 5000, NOT port 80!
Port 80 on NORDABIZ-01 runs nginx that redirects to HTTPS
Forwarding to port 80 causes infinite redirect loop
```

**Related Documentation:**
- Incident Report: `docs/INCIDENT_REPORT_20260102.md` (ERR_TOO_MANY_REDIRECTS)
- Container Diagram: `docs/architecture/02-container-diagram.md`
- Deployment Architecture: `docs/architecture/03-deployment-architecture.md`

---

## 1. Complete HTTP Request Flow

### 1.1 Successful Request Sequence Diagram

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
    participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
    participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
    participant DB as 💾 PostgreSQL<br/>localhost:5432

    Note over User,DB: SUCCESSFUL REQUEST FLOW

    User->>Browser: Navigate to https://nordabiznes.pl/
    Browser->>Fortigate: HTTPS GET / (Port 443)
    Note over Fortigate: NAT Translation<br/>85.237.177.83:443 → 10.22.68.250:443

    Fortigate->>NPM: HTTPS GET / (Port 443)
    Note over NPM: SSL/TLS Termination<br/>Let's Encrypt Certificate<br/>nordabiznes.pl (Cert ID: 27)

    Note over NPM: Request Processing<br/>• Validate certificate<br/>• Decrypt HTTPS<br/>• Extract headers<br/>• Check routing rules

    NPM->>Flask: HTTP GET / (Port 5000) ✓
    Note over NPM,Flask: ⚠️ CRITICAL: Port 5000<br/>NOT port 80!

    Note over Flask: Flask Request Handling<br/>• WSGI via Gunicorn<br/>• Route matching (app.py)<br/>• Session validation<br/>• CSRF check (if POST)

    Flask->>DB: SELECT * FROM companies WHERE status='active'
    DB->>Flask: Company data (80 rows)

    Note over Flask: Template Rendering<br/>• Jinja2 template: index.html<br/>• Inject company data<br/>• Apply filters & sorting

    Flask->>NPM: HTTP 200 OK<br/>Content-Type: text/html<br/>Set-Cookie: session=...<br/>HTML content

    Note over NPM: Response Processing<br/>• Encrypt response (HTTPS)<br/>• Add security headers<br/>• HSTS, CSP, X-Frame-Options

    NPM->>Fortigate: HTTPS 200 OK (Port 443)
    Fortigate->>Browser: HTTPS 200 OK
    Browser->>User: Display page
```

### 1.2 Failed Request (Wrong Port Configuration)

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
    participant NginxSys as ⚠️ Nginx System<br/>10.22.68.249:80
    participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000

    Note over User,Flask: FAILED REQUEST FLOW (REDIRECT LOOP)

    User->>Browser: Navigate to https://nordabiznes.pl/
    Browser->>NPM: HTTPS GET / (Port 443)

    Note over NPM: SSL/TLS Termination<br/>Decrypt HTTPS

    NPM->>NginxSys: ❌ HTTP GET / (Port 80)<br/>WRONG PORT!

    Note over NginxSys: Nginx System Config<br/>Redirects ALL HTTP to HTTPS

    NginxSys->>NPM: HTTP 301 Moved Permanently<br/>Location: https://nordabiznes.pl/

    Note over NPM: Follow redirect

    NPM->>NginxSys: HTTP GET / (Port 80)
    NginxSys->>NPM: HTTP 301 Moved Permanently<br/>Location: https://nordabiznes.pl/

    Note over NPM: Redirect loop detected<br/>After 20 redirects...

    NPM->>Browser: ERR_TOO_MANY_REDIRECTS
    Browser->>User: ❌ Error: Too many redirects

    Note over User: Portal UNAVAILABLE<br/>30 minutes downtime<br/>See: INCIDENT_REPORT_20260102.md
```

---

## 2. Layer-by-Layer Request Processing

### 2.1 Layer 1: Fortigate Firewall (NAT Gateway)

**Server:** Fortigate Firewall
**Public IP:** 85.237.177.83
**Function:** Network Address Translation (NAT) + Firewall

**Processing Steps:**

1. **Receive external request:**
   ```
   Source: User IP (e.g., 93.104.x.x)
   Destination: 85.237.177.83:443
   Protocol: HTTPS
   ```

2. **NAT Translation:**
   ```
   External: 85.237.177.83:443 → Internal: 10.22.68.250:443
   ```

3. **Firewall Rules:**
   - Allow TCP port 443 (HTTPS)
   - Allow TCP port 80 (HTTP, redirects to HTTPS)
   - Block all other inbound ports
   - Stateful connection tracking

4. **Forward to NPM:**
   ```
   Destination: 10.22.68.250:443 (NPM reverse proxy)
   Protocol: HTTPS (encrypted tunnel)
   ```

**Configuration:**
```
NAT Rule: DNAT 85.237.177.83:443 → 10.22.68.250:443
Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)
```

---

### 2.2 Layer 2: NPM Reverse Proxy (SSL Termination)

**Server:** R11-REVPROXY-01 (VM 119)
**IP:** 10.22.68.250
**Port:** 443 (HTTPS)
**Technology:** Nginx Proxy Manager (Docker)

**Processing Steps:**

1. **Receive HTTPS request:**
   ```
   Source: Fortigate (10.22.68.250:443)
   Method: GET
   Host: nordabiznes.pl
   Protocol: HTTPS/1.1 or HTTP/2
   ```

2. **SSL/TLS Termination:**
   - Load SSL certificate (Let's Encrypt, Certificate ID: 27)
   - Validate certificate (nordabiznes.pl, www.nordabiznes.pl)
   - Decrypt HTTPS traffic
   - Establish secure connection with client

3. **Request Header Processing:**
   ```http
   GET / HTTP/1.1
   Host: nordabiznes.pl
   User-Agent: Mozilla/5.0 ...
   Accept: text/html,application/xhtml+xml
   Accept-Language: pl-PL,pl;q=0.9
   Accept-Encoding: gzip, deflate, br
   Connection: keep-alive
   ```

4. **NPM Proxy Configuration Lookup:**
   ```sql
   -- NPM internal database query
   SELECT * FROM proxy_host WHERE id = 27;
   -- Result:
   domain_names: ["nordabiznes.pl", "www.nordabiznes.pl"]
   forward_scheme: "http"
   forward_host: "10.22.68.249"
   forward_port: 5000  ← CRITICAL!
   ssl_forced: true
   certificate_id: 27
   ```

5. **⚠️ CRITICAL ROUTING DECISION:**
   ```
   ✓ CORRECT: Forward to http://10.22.68.249:5000
   ❌ WRONG:   Forward to http://10.22.68.249:80 (causes redirect loop!)
   ```

6. **Forward to Backend (HTTP, unencrypted):**
   ```http
   GET / HTTP/1.1
   Host: nordabiznes.pl
   X-Real-IP: 93.104.x.x (original client IP)
   X-Forwarded-For: 93.104.x.x
   X-Forwarded-Proto: https
   X-Forwarded-Host: nordabiznes.pl
   Connection: close
   ```

**NPM Configuration (Proxy Host ID: 27):**

| Parameter | Value | Notes |
|-----------|-------|-------|
| Domain Names | nordabiznes.pl, www.nordabiznes.pl | Primary + www alias |
| Forward Scheme | http | NPM→Backend uses HTTP (secure internal network) |
| Forward Host | 10.22.68.249 | NORDABIZ-01 backend server |
| **Forward Port** | **5000** | **Flask/Gunicorn port (CRITICAL!)** |
| SSL Certificate | 27 (Let's Encrypt) | Auto-renewal enabled |
| SSL Forced | Yes | Redirect HTTP→HTTPS |
| HTTP/2 Support | Yes | Modern protocol support |
| HSTS Enabled | Yes | max-age=31536000; includeSubDomains |
| Block Exploits | Yes | Nginx security module |
| Websocket Support | Yes | For future features |

**Verification Command:**
```bash
# Check NPM configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
  sqlite3 /data/database.sqlite \
  \"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""

# Expected output:
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000
```

---

### 2.3 Layer 3: Flask/Gunicorn Application (Request Processing)

**Server:** NORDABIZ-01 (VM 249)
**IP:** 10.22.68.249
**Port:** 5000
**Technology:** Gunicorn 20.1.0 + Flask 3.0

**Processing Steps:**

1. **Gunicorn Receives HTTP Request:**
   ```
   Binding: 0.0.0.0:5000 (all interfaces)
   Workers: 4 (Gunicorn worker processes)
   Worker Class: sync (synchronous workers)
   Timeout: 120 seconds
   ```

2. **Worker Selection:**
   - Gunicorn master process receives connection
   - Distributes request to available worker (round-robin)
   - Worker loads WSGI app (Flask application)

3. **WSGI Interface:**
   ```python
   # Gunicorn calls Flask's WSGI application
   from app import app

   # WSGI environ dict contains:
   environ = {
       'REQUEST_METHOD': 'GET',
       'PATH_INFO': '/',
       'QUERY_STRING': '',
       'SERVER_NAME': 'nordabiznes.pl',
       'SERVER_PORT': '5000',
       'HTTP_HOST': 'nordabiznes.pl',
       'HTTP_X_REAL_IP': '93.104.x.x',
       'HTTP_X_FORWARDED_PROTO': 'https',
       'wsgi.url_scheme': 'http',
       # ... more headers
   }
   ```

4. **Flask Request Handling (app.py):**

   **a) Request Context Setup:**
   ```python
   # Flask creates request context
   from flask import request, session, g

   # Parse request
   request.method = 'GET'
   request.path = '/'
   request.url = 'https://nordabiznes.pl/'
   request.args = {}  # Query parameters
   request.headers = {...}  # HTTP headers
   ```

   **b) Before Request Hooks:**
   ```python
   @app.before_request
   def load_logged_in_user():
       # Check session for user_id
       user_id = session.get('user_id')
       if user_id:
           g.user = db.session.query(User).get(user_id)
       else:
           g.user = None
   ```

   **c) Route Matching:**
   ```python
   # Flask router matches route
   @app.route('/')
   def index():
       # Main catalog page
       pass
   ```

   **d) View Function Execution:**
   ```python
   @app.route('/')
   def index():
       # Query companies from database
       companies = db.session.query(Company)\
           .filter_by(status='active')\
           .order_by(Company.name)\
           .all()  # 80 companies

       # Render template
       return render_template('index.html', companies=companies)
   ```

5. **Database Query (PostgreSQL):**
   ```python
   # SQLAlchemy ORM generates SQL
   SELECT * FROM companies
   WHERE status = 'active'
   ORDER BY name;
   ```

6. **Template Rendering (Jinja2):**
   ```python
   # templates/index.html
   from jinja2 import Environment, FileSystemLoader

   # Render template with context
   html = render_template('index.html',
                          companies=companies,
                          user=g.user,
                          config=app.config)
   ```

7. **Response Construction:**
   ```python
   # Flask creates HTTP response
   response = Response(
       response=html,  # Rendered HTML
       status=200,     # HTTP status code
       headers={
           'Content-Type': 'text/html; charset=utf-8',
           'Content-Length': len(html),
           'Set-Cookie': 'session=...; HttpOnly; Secure; SameSite=Lax'
       }
   )
   ```

**Gunicorn Configuration:**
```ini
# /etc/systemd/system/nordabiznes.service
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/nordabiznes
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
    --bind 0.0.0.0:5000 \
    --workers 4 \
    --timeout 120 \
    --access-logfile /var/log/nordabiznes/access.log \
    --error-logfile /var/log/nordabiznes/error.log \
    --log-level info \
    app:app
```

**Verification Command:**
```bash
# Test Flask directly (from server)
curl -I http://10.22.68.249:5000/health
# Expected: HTTP/1.1 200 OK

# Check Gunicorn workers
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
# Expected: 1 master + 4 worker processes
```

---

### 2.4 Layer 4: PostgreSQL Database (Data Retrieval)

**Server:** NORDABIZ-01 (same server as Flask)
**IP:** 127.0.0.1 (localhost only)
**Port:** 5432
**Technology:** PostgreSQL 14

**Processing Steps:**

1. **Receive SQL Query:**
   ```sql
   -- SQLAlchemy ORM generates query
   SELECT companies.id, companies.name, companies.slug,
          companies.short_description, companies.category,
          companies.nip, companies.city, companies.website
   FROM companies
   WHERE companies.status = 'active'
   ORDER BY companies.name;
   ```

2. **Query Execution:**
   - Parse SQL syntax
   - Create execution plan (Query planner)
   - Use indexes if available (idx_companies_status, idx_companies_name)
   - Fetch rows from disk/cache

3. **Return Results:**
   ```python
   # SQLAlchemy ORM returns Company objects
   [
       Company(id=1, name='ALMARES', slug='almares', ...),
       Company(id=2, name='AMA', slug='ama-spolka-z-o-o', ...),
       # ... 78 more companies
   ]
   ```

**Connection Configuration:**
```python
# app.py database connection
DATABASE_URL = 'postgresql://nordabiz_app:PASSWORD@localhost:5432/nordabiz'

# SQLAlchemy engine config
engine = create_engine(DATABASE_URL,
    pool_size=10,          # Connection pool
    max_overflow=20,       # Additional connections
    pool_pre_ping=True,    # Validate connections
    pool_recycle=3600      # Recycle after 1 hour
)
```

**Security:**
- PostgreSQL listens on **127.0.0.1 ONLY** (no external access)
- Application connects via localhost socket
- User: `nordabiz_app` (limited privileges, no DROP/CREATE)
- SSL: Not required (localhost connection)

**Verification:**
```bash
# Check PostgreSQL status
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"

# Test connection (from server)
ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 -d nordabiz -c 'SELECT COUNT(*) FROM companies;'"
# Expected: 80
```

---

## 3. Response Flow (Flask → User)

### 3.1 Response Path Sequence

```mermaid
sequenceDiagram
    participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
    participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
    participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
    participant Browser

    Note over Flask: Response Construction
    Flask->>Flask: Render Jinja2 template<br/>HTML content (50 KB)

    Flask->>NPM: HTTP/1.1 200 OK<br/>Content-Type: text/html; charset=utf-8<br/>Content-Length: 51234<br/>Set-Cookie: session=...<br/><br/>[HTML content]

    Note over NPM: Response Processing
    NPM->>NPM: Add security headers:<br/>• Strict-Transport-Security<br/>• X-Frame-Options: SAMEORIGIN<br/>• X-Content-Type-Options: nosniff<br/>• Referrer-Policy: strict-origin

    NPM->>NPM: Compress response (gzip)<br/>Size: 50 KB → 12 KB

    NPM->>NPM: Encrypt response (TLS 1.3)<br/>Certificate: Let's Encrypt

    NPM->>Fortigate: HTTPS 200 OK<br/>Encrypted response<br/>Size: 12 KB (gzip)

    Note over Fortigate: NAT reverse translation
    Fortigate->>Browser: HTTPS 200 OK<br/>Source: 85.237.177.83

    Note over Browser: Response Handling
    Browser->>Browser: Decrypt HTTPS<br/>Decompress gzip<br/>Parse HTML<br/>Render page
```

### 3.2 Response Headers (NPM → User)

**Headers Added by NPM:**
```http
HTTP/2 200 OK
server: nginx/1.24.0 (Ubuntu)
date: Fri, 10 Jan 2026 10:30:00 GMT
content-type: text/html; charset=utf-8
content-length: 12345
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'
```

**Headers from Flask (preserved):**
```http
set-cookie: session=eyJ...; HttpOnly; Path=/; SameSite=Lax; Secure
vary: Cookie
x-request-id: abc123def456
```

**Response Body:**
```html
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Norda Biznes Partner - Katalog firm</title>
    <!-- ... CSS, meta tags ... -->
</head>
<body>
    <!-- Rendered company catalog -->
</body>
</html>
```

---

## 4. Port Mapping Reference

### 4.1 Complete Port Flow

```
┌─────────────────────────────────────────────────────────────────┐
│ EXTERNAL USER                                                    │
│ Browser: https://nordabiznes.pl/                                │
└────────────────────────────┬────────────────────────────────────┘
                             │ HTTPS (Port 443)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ FORTIGATE FIREWALL                                              │
│ Public IP: 85.237.177.83:443                                    │
│ NAT: 85.237.177.83:443 → 10.22.68.250:443                      │
└────────────────────────────┬────────────────────────────────────┘
                             │ HTTPS (Port 443)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ NPM REVERSE PROXY (R11-REVPROXY-01)                            │
│ IP: 10.22.68.250:443                                           │
│ Function: SSL Termination                                       │
│ Certificate: Let's Encrypt (nordabiznes.pl)                    │
│                                                                 │
│ ⚠️ CRITICAL ROUTING DECISION:                                  │
│ ✓ Forward to: http://10.22.68.249:5000  (CORRECT)             │
│ ❌ DO NOT use: http://10.22.68.249:80    (WRONG!)              │
└────────────────────────────┬────────────────────────────────────┘
                             │ HTTP (Port 5000) ✓
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ FLASK/GUNICORN (NORDABIZ-01)                                   │
│ IP: 10.22.68.249:5000                                          │
│ Binding: 0.0.0.0:5000                                          │
│ Workers: 4 (Gunicorn)                                          │
│ Function: Application logic, template rendering                │
└────────────────────────────┬────────────────────────────────────┘
                             │ SQL (localhost:5432)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ POSTGRESQL (NORDABIZ-01)                                       │
│ IP: 127.0.0.1:5432 (localhost only)                           │
│ Database: nordabiz                                             │
│ Function: Data storage                                         │
└─────────────────────────────────────────────────────────────────┘
```

### 4.2 Port Table (NORDABIZ-01)

| Port | Service | Binding | User | Purpose | NPM Should Use? |
|------|---------|---------|------|---------|-----------------|
| **5000** | **Gunicorn/Flask** | **0.0.0.0** | **www-data** | **Main Application** | **✓ YES** |
| 80 | Nginx (system) | 0.0.0.0 | root | HTTP→HTTPS redirect | ❌ NO (causes loop!) |
| 443 | Nginx (system) | 0.0.0.0 | root | HTTPS redirect | ❌ NO (NPM handles SSL) |
| 5432 | PostgreSQL | 127.0.0.1 | postgres | Database | ❌ NO (localhost only) |
| 22 | SSH | 0.0.0.0 | - | Administration | ❌ NO (not HTTP) |

**⚠️ CRITICAL WARNING:**
```
Port 80 and 443 on NORDABIZ-01 run a system nginx that:
1. Redirects ALL HTTP requests to HTTPS
2. Redirects ALL HTTPS requests to https://nordabiznes.pl

If NPM forwards to port 80:
  NPM (HTTPS) → Nginx (port 80) → 301 redirect to HTTPS
  → NPM receives redirect → forwards to port 80 again
  → INFINITE LOOP → ERR_TOO_MANY_REDIRECTS

SOLUTION: NPM must ALWAYS forward to port 5000!
```

---

## 5. Request Types and Routing

### 5.1 Static Assets

**Request:** `GET https://nordabiznes.pl/static/css/style.css`

**Flow:**
```
User → NPM → Flask → Static file handler → Return CSS
```

**Flask Handling:**
```python
# Flask serves static files from /static directory
@app.route('/static/<path:filename>')
def static_files(filename):
    return send_from_directory('static', filename)
```

**Optimization:** NPM could cache static assets (future enhancement)

---

### 5.2 API Endpoints

**Request:** `GET https://nordabiznes.pl/api/companies`

**Flow:**
```
User → NPM → Flask → API route → Database → JSON response
```

**Response:**
```json
{
  "companies": [
    {
      "id": 1,
      "name": "PIXLAB",
      "slug": "pixlab-sp-z-o-o",
      "category": "IT"
    }
  ],
  "total": 80
}
```

**Headers:**
```http
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: * (if CORS enabled)
```

---

### 5.3 Form Submissions (POST)

**Request:** `POST https://nordabiznes.pl/login`

**Flow:**
```
User → NPM → Flask → CSRF validation → Auth check → Database → Redirect
```

**Additional Processing:**
- **CSRF Token Validation:** Flask-WTF checks token in form
- **Session Creation:** Flask-Login creates session cookie
- **Database Update:** Update `last_login` timestamp

**Response:**
```http
HTTP/2 302 Found
Location: https://nordabiznes.pl/dashboard
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
```

---

### 5.4 Health Check Endpoint

**Request:** `GET https://nordabiznes.pl/health`

**Purpose:** Monitoring and verification

**Flow:**
```
Monitor → NPM → Flask → Simple response (no DB query)
```

**Response:**
```http
HTTP/2 200 OK
Content-Type: application/json

{
  "status": "healthy",
  "timestamp": "2026-01-10T10:30:00Z"
}
```

**Use Cases:**
- Verify NPM configuration after changes
- External monitoring (Zabbix)
- Load balancer health checks (future)

---

## 6. Security Layers

### 6.1 Security Boundaries

```mermaid
graph TB
    Internet["🌐 Internet<br/>(Untrusted)"]

    subgraph "Security Zone 1: DMZ"
        Fortigate["🛡️ Fortigate Firewall<br/>• NAT<br/>• DPI<br/>• IPS"]
        NPM["🔒 NPM Reverse Proxy<br/>• SSL/TLS Termination<br/>• HSTS<br/>• Rate Limiting"]
    end

    subgraph "Security Zone 2: Application Layer"
        Flask["🌐 Flask Application<br/>• CSRF Protection<br/>• Input Validation<br/>• XSS Prevention<br/>• Session Management"]
    end

    subgraph "Security Zone 3: Data Layer"
        DB["💾 PostgreSQL<br/>• No external access<br/>• User privileges<br/>• SQL injection protection"]
    end

    Internet --> Fortigate
    Fortigate --> NPM
    NPM --> Flask
    Flask --> DB

    classDef zoneStyle fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef appStyle fill:#3498db,stroke:#2980b9,color:#fff
    classDef dataStyle fill:#2ecc71,stroke:#27ae60,color:#fff

    class Fortigate,NPM zoneStyle
    class Flask appStyle
    class DB dataStyle
```

### 6.2 Security Features by Layer

**Layer 1: Fortigate Firewall**
- Deep Packet Inspection (DPI)
- Intrusion Prevention System (IPS)
- Rate limiting (connection-level)
- Geo-blocking (if configured)

**Layer 2: NPM Reverse Proxy**
- SSL/TLS 1.3 encryption
- HSTS (HTTP Strict Transport Security)
- Certificate validation
- Security headers injection
- Request size limits

**Layer 3: Flask Application**
- Flask-Login session management
- CSRF token validation (Flask-WTF)
- Input sanitization (XSS prevention)
- SQL injection protection (SQLAlchemy ORM)
- Rate limiting (Flask-Limiter)
- Authentication & Authorization

**Layer 4: PostgreSQL Database**
- Localhost-only binding (127.0.0.1)
- User privilege separation (nordabiz_app)
- Parameterized queries (SQLAlchemy)
- Connection pooling with limits

---

## 7. Performance Metrics

### 7.1 Typical Request Latency

| Layer | Processing Time | Notes |
|-------|-----------------|-------|
| Fortigate NAT | < 1 ms | Hardware NAT, negligible latency |
| NPM SSL Termination | 10-20 ms | TLS handshake + decryption |
| NPM → Flask Network | < 1 ms | Internal 10 Gbps network |
| Flask Request Handling | 50-150 ms | Route matching, template rendering |
| Database Query | 10-30 ms | Indexed queries, connection pool |
| Template Rendering | 20-50 ms | Jinja2 template compilation |
| NPM SSL Encryption | 5-10 ms | Response encryption |
| **Total (typical)** | **96-262 ms** | **Median: ~180 ms** |

**Factors Affecting Latency:**
- Database query complexity (JOIN operations, FTS)
- Template size (company catalog vs. simple page)
- Gunicorn worker availability (max 4 concurrent)
- Network congestion (internal or external)

### 7.2 Throughput

**Gunicorn Workers:** 4 synchronous workers

**Concurrent Requests:** Max 4 simultaneous requests

**Requests per Second (RPS):**
- Simple pages (health check): ~20-30 RPS
- Database queries (catalog): ~5-10 RPS
- Complex AI chat: ~2-3 RPS (limited by Gemini API)

**Bottlenecks:**
1. Gunicorn worker count (4 workers)
2. PostgreSQL connection pool (max 30)
3. AI API rate limits (1,500 req/day Gemini)

---

## 8. Troubleshooting Guide

### 8.1 Common Issues and Diagnostics

#### Issue 1: ERR_TOO_MANY_REDIRECTS

**Symptoms:**
- Browser error: "ERR_TOO_MANY_REDIRECTS"
- Portal inaccessible from external network
- Works from internal network (10.22.68.0/24)

**Root Cause:**
- NPM forwarding to port 80 instead of 5000

**Diagnosis:**
```bash
# 1. Check NPM configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
  sqlite3 /data/database.sqlite \
  \"SELECT forward_port FROM proxy_host WHERE id = 27;\""

# Expected: 5000
# If shows: 80 ← PROBLEM!

# 2. Test direct backend access
curl -I http://10.22.68.249:80/
# If returns: HTTP 301 → Problem confirmed

curl -I http://10.22.68.249:5000/
# Should return: HTTP 200 OK
```

**Solution:**
```bash
# Update NPM configuration via API or UI
# Set forward_port to 5000

# Verify fix
curl -I https://nordabiznes.pl/health
# Expected: HTTP 200 OK
```

**Reference:** `docs/INCIDENT_REPORT_20260102.md`

---

#### Issue 2: 502 Bad Gateway

**Symptoms:**
- NPM returns "502 Bad Gateway"
- Error in NPM logs: "connect() failed (111: Connection refused)"

**Root Cause:**
- Flask/Gunicorn not running
- Flask listening on wrong port
- Firewall blocking port 5000

**Diagnosis:**
```bash
# 1. Check if Gunicorn is running
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
# Expected: Active (running)

# 2. Check if port 5000 is listening
ssh maciejpi@10.22.68.249 "sudo netstat -tlnp | grep 5000"
# Expected: 0.0.0.0:5000 ... gunicorn

# 3. Test direct connection
curl -I http://10.22.68.249:5000/health
# Expected: HTTP 200 OK
```

**Solution:**
```bash
# Restart Gunicorn
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"

# Check logs
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 50"
```

---

#### Issue 3: 504 Gateway Timeout

**Symptoms:**
- Request takes > 60 seconds
- NPM returns "504 Gateway Timeout"

**Root Cause:**
- Flask processing takes too long
- Database query timeout
- External API timeout (Gemini, PageSpeed)

**Diagnosis:**
```bash
# 1. Check Gunicorn worker status
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
# Look for workers in state 'R' (running) vs 'S' (sleeping)

# 2. Check application logs
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"

# 3. Check database connections
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
  \"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
```

**Solution:**
- Increase Gunicorn timeout (default: 120s)
- Optimize slow database queries
- Add timeout to external API calls

---

#### Issue 4: SSL Certificate Error

**Symptoms:**
- Browser warning: "Your connection is not private"
- Certificate expired or invalid

**Root Cause:**
- Let's Encrypt certificate expired
- Certificate renewal failed
- Wrong certificate for domain

**Diagnosis:**
```bash
# Check certificate expiry
echo | openssl s_client -servername nordabiznes.pl \
  -connect 85.237.177.83:443 2>/dev/null | openssl x509 -noout -dates

# Expected: notAfter date in the future
```

**Solution:**
```bash
# Renew certificate via NPM UI or API
# NPM auto-renews 30 days before expiry
```

---

### 8.2 Diagnostic Commands Reference

**Quick Health Check:**
```bash
# External access test
curl -I https://nordabiznes.pl/health
# Expected: HTTP/2 200 OK

# Internal access test (from INPI network)
curl -I http://10.22.68.249:5000/health
# Expected: HTTP/1.1 200 OK
```

**NPM Configuration Check:**
```bash
# Show proxy host configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
  sqlite3 /data/database.sqlite \
  \"SELECT id, domain_names, forward_host, forward_port, ssl_forced \
   FROM proxy_host WHERE id = 27;\""
```

**Application Status:**
```bash
# Service status
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"

# Worker processes
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn | grep -v grep"

# Recent logs
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 20 --no-pager"
```

**Database Status:**
```bash
# PostgreSQL status
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"

# Connection count
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
  \"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""

# Database size
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
  \"SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
```

**Network Connectivity:**
```bash
# Test NPM → Flask connectivity
ssh maciejpi@10.22.68.250 "curl -I http://10.22.68.249:5000/health"

# Test Flask → Database connectivity
ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 \
  -d nordabiz -c 'SELECT 1;'"
```

---

## 9. Configuration Reference

### 9.1 NPM Proxy Host Configuration

**File Location (Docker):** `/data/database.sqlite` (inside NPM container)

**Proxy Host ID:** 27

**Complete Configuration:**
```json
{
  "id": 27,
  "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
  "forward_scheme": "http",
  "forward_host": "10.22.68.249",
  "forward_port": 5000,
  "access_list_id": 0,
  "certificate_id": 27,
  "ssl_forced": true,
  "caching_enabled": false,
  "block_exploits": true,
  "advanced_config": "",
  "allow_websocket_upgrade": true,
  "http2_support": true,
  "hsts_enabled": true,
  "hsts_subdomains": true
}
```

**How to Update (NPM API):**
```python
import requests

NPM_URL = "http://10.22.68.250:81/api"
# Login to get token first, then:

data = {
    "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
    "forward_scheme": "http",
    "forward_host": "10.22.68.249",
    "forward_port": 5000,  # CRITICAL!
    "certificate_id": 27,
    "ssl_forced": True,
    "http2_support": True,
    "hsts_enabled": True
}

response = requests.put(
    f"{NPM_URL}/nginx/proxy-hosts/27",
    headers={"Authorization": f"Bearer {token}"},
    json=data
)
```

---

### 9.2 Gunicorn Configuration

**Systemd Unit File:** `/etc/systemd/system/nordabiznes.service`

```ini
[Unit]
Description=Norda Biznes Partner Flask Application
After=network.target postgresql.service

[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/var/www/nordabiznes
Environment="PATH=/var/www/nordabiznes/venv/bin"
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
    --bind 0.0.0.0:5000 \
    --workers 4 \
    --worker-class sync \
    --timeout 120 \
    --keep-alive 5 \
    --access-logfile /var/log/nordabiznes/access.log \
    --error-logfile /var/log/nordabiznes/error.log \
    --log-level info \
    --capture-output \
    app:app

Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
```

**Parameters Explained:**
- `--bind 0.0.0.0:5000` - Listen on all interfaces, port 5000
- `--workers 4` - 4 worker processes (matches CPU cores)
- `--worker-class sync` - Synchronous workers (default)
- `--timeout 120` - 120 second request timeout
- `--keep-alive 5` - 5 second keep-alive for connections

---

### 9.3 Flask Application Configuration

**Environment Variables (.env):**
```bash
# Database
DATABASE_URL=postgresql://nordabiz_app:PASSWORD@localhost:5432/nordabiz

# Flask
SECRET_KEY=random_secret_key_here
FLASK_ENV=production

# Security
SESSION_COOKIE_SECURE=True
SESSION_COOKIE_HTTPONLY=True
SESSION_COOKIE_SAMESITE=Lax

# External APIs
GOOGLE_API_KEY=AIza...
BRAVE_API_KEY=BSA...
MS_GRAPH_CLIENT_ID=abc...
MS_GRAPH_CLIENT_SECRET=def...
```

**Flask Config (app.py):**
```python
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
```

---

## 10. Monitoring and Logging

### 10.1 Log Locations

**NPM Logs (Docker):**
```bash
# Access logs
ssh maciejpi@10.22.68.250 "docker logs nginx-proxy-manager_app_1 --tail 100"

# Follow logs in real-time
ssh maciejpi@10.22.68.250 "docker logs -f nginx-proxy-manager_app_1"
```

**Gunicorn Logs:**
```bash
# Application logs (systemd journal)
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -f"

# Access logs (file-based)
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/access.log"

# Error logs
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"
```

**PostgreSQL Logs:**
```bash
# Query logs (if enabled)
ssh maciejpi@10.22.68.249 "sudo tail -f /var/log/postgresql/postgresql-14-main.log"
```

### 10.2 Key Metrics to Monitor

**HTTP Metrics (NPM):**
- Requests per second (RPS)
- HTTP status codes (200, 301, 404, 500, 502, 504)
- Response time (p50, p95, p99)
- SSL certificate expiry

**Application Metrics (Gunicorn):**
- Worker utilization (all 4 workers busy?)
- Request timeout rate
- Error rate (5xx responses)
- Memory usage per worker

**Database Metrics (PostgreSQL):**
- Connection count (should be < 30)
- Query execution time
- Database size growth
- Table bloat

**System Metrics (NORDABIZ-01):**
- CPU usage (should be < 80%)
- Memory usage (should be < 6 GB / 8 GB)
- Disk I/O (should be low)
- Network throughput

---

## 11. Future Enhancements

### 11.1 Planned Improvements

**1. CDN Integration**
- Cloudflare or custom CDN for static assets
- Reduces load on NPM and Flask
- Improves global latency

**2. Load Balancing**
- Multiple Flask backend instances
- NPM upstream configuration
- Session affinity handling

**3. Caching Layer**
- Redis cache for frequent queries
- Page cache for anonymous users
- API response caching

**4. Monitoring System**
- Zabbix integration for health checks
- Alert on 5xx errors, high latency
- Dashboard for key metrics

**5. Rate Limiting**
- NPM-level rate limiting by IP
- Protection against DDoS
- API endpoint throttling

---

## 12. Related Documentation

- **Incident Report:** `docs/INCIDENT_REPORT_20260102.md` - NPM port configuration incident
- **System Context:** `docs/architecture/01-system-context.md` - High-level system view
- **Container Diagram:** `docs/architecture/02-container-diagram.md` - Container architecture
- **Deployment Architecture:** `docs/architecture/03-deployment-architecture.md` - Infrastructure details
- **Flask Components:** `docs/architecture/04-flask-components.md` - Application structure
- **Authentication Flow:** `docs/architecture/flows/01-authentication-flow.md` - User authentication
- **Search Flow:** `docs/architecture/flows/02-search-flow.md` - Company search process
- **CLAUDE.md:** Main project documentation with NPM configuration reference

---

## Glossary

- **NPM**: Nginx Proxy Manager - Docker-based reverse proxy with web UI
- **NAT**: Network Address Translation - Fortigate translates external IP to internal
- **SSL Termination**: Decrypting HTTPS at NPM, forwarding HTTP to backend
- **HSTS**: HTTP Strict Transport Security - Forces browsers to use HTTPS
- **WSGI**: Web Server Gateway Interface - Python web application interface
- **Gunicorn**: Python WSGI HTTP server (Green Unicorn)
- **Round Robin**: Load balancing method distributing requests evenly
- **ERR_TOO_MANY_REDIRECTS**: Browser error when redirect loop detected (> 20 redirects)

---

**Document Status:** Production-ready, verified against live system
**Last Incident:** 2026-01-02 (ERR_TOO_MANY_REDIRECTS due to port 80 configuration)
**Next Review:** After any NPM configuration changes

---

*This document was created as part of the architecture documentation initiative to prevent configuration incidents and improve system understanding.*
