diff --git a/reference/esp32-agent/README.md b/reference/esp32-agent/README.md
new file mode 100644
index 0000000..9644edf
--- /dev/null
+++ b/reference/esp32-agent/README.md
@@ -0,0 +1,103 @@
+# ESP32 Agent — Architecture Reference
+
+**Status**: ARCHITECTURE ONLY (no code)
+**Location**: `reference/esp32-agent/` (NOT a submodule)
+
+## Overview
+
+This directory contains the architecture design and documentation for the ESP32-based WireGuard agent. It is **NOT a git submodule** — it is an architecture reference for future implementation.
+
+## Why `reference/` not `apps/`?
+
+If ESP32 agent is implemented as a submodule in the future, it will be added to `apps/device-agent-embedded/`. This `reference/` directory stores the architecture docs separately to avoid conflicts.
+
+## Purpose
+
+- Document the ESP32 agent architecture
+- Define the API contract (shared with server-core)
+- Hardware reference for LW840X module
+- Track implementation status and TODOs
+
+## When to Implement
+
+When ready to build the ESP32 agent:
+
+1. Create a new repo: `nexus-agent-embedded`
+2. Add as submodule: `apps/device-agent-embedded/`
+3. Use ESP-IDF v5.2+ toolchain
+4. Follow the architecture in `docs/DESIGN.md`
+
+## Files
+
+| File | Description |
+|------|-------------|
+| `docs/DESIGN.md` | Technical design document (full architecture) |
+| `docs/HARDWARE.md` | ESP32/LW840X hardware reference |
+| `docs/API_COMPAT.md` | Server API contract (shared protocol) |
+| `docs/ARCHITECTURE.md` | Module architecture + implementation status |
+
+## Architecture Summary
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ ESP32 (FreeRTOS) │
+├─────────────────────────────────────────────────────────┤
+│ Application Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ provision│ │ heartbeat│ │ tunnel │ │ config │ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
+│ │ │ │ │ │
+│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
+│ │ crypto.c │ │
+│ │ AES-256-GCM decrypt (mbedtls) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Network Stack (lwIP) │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ WireGuard (lwIP socket API) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Hardware Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ WiFi │ │ NVS │ │ UART │ │ GPIO │ │
+│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+## Implementation Status
+
+| Module | Status | Notes |
+|--------|--------|-------|
+| `tunnel.c` | ❌ STUB | Needs `wireguard-esp32` component integration |
+| `heartbeat.c` | ✅ Real | HTTP loop working, config sync TODO |
+| `provision.c` | ✅ Real | HTTP provisioning working |
+| `crypto.c` | ✅ Real | mbedtls AES-256-GCM |
+| `config.c` | ✅ Real | NVS storage |
+| `wifi.c` | ✅ Real | WiFi STA management |
+
+## Dependencies
+
+- **ESP-IDF**: v5.2+
+- **mbedtls**: AES-256-GCM, SHA-256 (built-in)
+- **lwIP**: TCP/IP stack (built-in)
+- **FreeRTOS**: RTOS (built-in)
+- **wireguard-esp32**: WireGuard tunnel library (NOT YET INTEGRATED)
+
+## API Contract (Shared with Server)
+
+```
+POST /api/v1/provision → { token, hwid } → { encrypted_config }
+POST /api/v1/heartbeat → { device_id, tunnel_up, last_handshake } → { config sync }
+```
+
+See `docs/API_COMPAT.md` for full specification.
+
+## Related
+
+- [Server Core API](../../server-core/docs/)
+- [Device Agent (Go)](../../apps/device-agent/)
+- [Android Agent](../../apps/android-agent/)
+
+## License
+
+Proprietary - NexusGuard
diff --git a/reference/esp32-agent/docs/API_COMPAT.md b/reference/esp32-agent/docs/API_COMPAT.md
new file mode 100644
index 0000000..21f79ab
--- /dev/null
+++ b/reference/esp32-agent/docs/API_COMPAT.md
@@ -0,0 +1,335 @@
+# API Compatibility Reference
+
+**Version**: 1.0.0
+**Date**: 2026-06-26
+
+This document defines the HTTP API contract between NexusGuard Server Core and Embedded Agent (ESP32).
+
+## Overview
+
+The embedded agent communicates with the server via two HTTP endpoints:
+
+1. **Provisioning**: `POST /api/v1/provision` — Initial key exchange
+2. **Heartbeat**: `POST /api/v1/heartbeat` — Periodic status + config sync
+
+## 1. Provisioning
+
+### Request
+
+```
+POST /api/v1/provision
+Content-Type: application/json
+```
+
+```json
+{
+ "token": "string (required)",
+ "hwid": "string (required)"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `token` | string | Yes | Registration token (single-use) |
+| `hwid` | string | Yes | Hardware ID (ESP32 MAC or serial) |
+
+### Response (Success)
+
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+```
+
+```json
+{
+ "encrypted_config": "base64-encoded-bytes"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `encrypted_config` | string | AES-256-GCM encrypted ConfigPayload |
+
+### Response (Error)
+
+```json
+{
+ "error": "string"
+}
+```
+
+| Status | Error | Description |
+|--------|-------|-------------|
+| 400 | `"invalid token format"` | Token is not valid UUID |
+| 400 | `"token already used"` | Token was already consumed |
+| 400 | `"invalid hardware id"` | HWID is empty or invalid |
+| 403 | `"token expired"` | Token has expired (24h TTL) |
+| 500 | `"provisioning failed"` | Server internal error |
+
+### Decrypted ConfigPayload
+
+After decrypting `encrypted_config`, the agent receives:
+
+```json
+{
+ "device_id": "string (UUID)",
+ "private_key": "string (hex, 64 chars)",
+ "preshared_key": "string (hex, 64 chars)",
+ "internal_ip": "string (e.g. 10.172.21.2)",
+ "server_pub": "string (hex, 64 chars)",
+ "endpoint": "string (e.g. italy-twenty.gl.at.ply.gg:59750)",
+ "dns": "string (e.g. 1.1.1.1)",
+ "allowed_ips": "string (e.g. 0.0.0.0/0)",
+ "server_wg_ip": "string (e.g. 10.172.21.1)"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `device_id` | string | Device UUID (use in heartbeat) |
+| `private_key` | string | WireGuard private key (hex) |
+| `preshared_key` | string | Pre-shared key (hex) |
+| `internal_ip` | string | Device IP address |
+| `server_pub` | string | Server WireGuard public key (hex) |
+| `endpoint` | string | Server WireGuard endpoint |
+| `dns` | string | DNS server IP |
+| `allowed_ips` | string | Allowed IPs (e.g. "0.0.0.0/0" for full tunnel) |
+| `server_wg_ip` | string | Server WireGuard IP |
+
+## 2. Heartbeat
+
+### Request
+
+```
+POST /api/v1/heartbeat
+Content-Type: application/json
+```
+
+```json
+{
+ "device_id": "string (required)",
+ "status": "string (optional)",
+ "state": "string (optional)",
+ "tunnel_up": "boolean (optional)",
+ "last_handshake": "string (optional, ISO 8601)"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `device_id` | string | Yes | Device UUID |
+| `status` | string | No | "connected", "recovering", "stopped" |
+| `state` | string | No | Free-form state description |
+| `tunnel_up` | boolean | No | WireGuard tunnel status |
+| `last_handshake` | string | No | ISO 8601 timestamp of last handshake |
+
+### Response (Success)
+
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+```
+
+```json
+{
+ "device_id": "string",
+ "status": "ok",
+ "internal_ip": "string",
+ "private_key": "string (hex)",
+ "preshared_key": "string (hex)",
+ "server_pub": "string (hex)",
+ "endpoint": "string",
+ "server_wg_ip": "string",
+ "dns": "string",
+ "allowed_ips": "string",
+ "forwards_hash": "string"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `device_id` | string | Device UUID |
+| `status` | string | "ok" or error |
+| `internal_ip` | string | Device IP (may change) |
+| `private_key` | string | WireGuard private key (may rotate) |
+| `preshared_key` | string | Pre-shared key (may rotate) |
+| `server_pub` | string | Server public key |
+| `endpoint` | string | Server endpoint |
+| `server_wg_ip` | string | Server WireGuard IP |
+| `dns` | string | DNS server |
+| `allowed_ips` | string | Allowed IPs |
+| `forwards_hash` | string | Hash of port forwards (for change detection) |
+
+### Response (Error)
+
+```json
+{
+ "error": "string"
+}
+```
+
+| Status | Error | Description |
+|--------|-------|-------------|
+| 400 | `"invalid device id"` | Device ID is not valid UUID |
+| 403 | `"device is suspended"` | Device is suspended by admin |
+| 500 | `"heartbeat failed"` | Server internal error |
+
+## 3. Crypto Protocol
+
+### Key Derivation
+
+```
+key = SHA256(hwid + salt)
+```
+
+- `hwid`: Hardware ID (ESP32 MAC or serial)
+- `salt`: Server's `SERVER_SALT` environment variable
+- Output: 32-byte AES key
+
+### Encryption (Server Side)
+
+```
+plaintext = ConfigPayload JSON
+nonce = random 12 bytes
+ciphertext = AES-256-GCM-Encrypt(key, nonce, plaintext)
+encrypted_config = nonce + ciphertext
+```
+
+### Decryption (Agent Side)
+
+```
+encrypted_config = response.encrypted_config (base64 decoded)
+nonce = encrypted_config[0:12]
+ciphertext = encrypted_config[12:]
+plaintext = AES-256-GCM-Decrypt(key, nonce, ciphertext)
+ConfigPayload = JSON.parse(plaintext)
+```
+
+### Implementation Notes
+
+- Use `mbedtls_sha256()` for key derivation
+- Use `mbedtls_gcm_*()` for AES-256-GCM
+- Nonce size: 12 bytes (fixed for GCM)
+- Tag size: 16 bytes (default for GCM)
+
+## 4. Config Change Detection
+
+### Heartbeat Hash Comparison
+
+The server returns `forwards_hash` in heartbeat response. Agent should:
+
+1. Store last received `forwards_hash` in NVS
+2. Compare with current hash
+3. If different, trigger config reload
+
+```c
+// Pseudo-code
+char last_hash[64];
+nvs_get_str(nvs_handle, "config_hash", last_hash, sizeof(last_hash));
+
+if (strcmp(last_hash, response.forwards_hash) != 0) {
+ // Config changed, rebuild tunnel
+ tunnel_rebuild(&config);
+ nvs_set_str(nvs_handle, "config_hash", response.forwards_hash);
+}
+```
+
+## 5. Versioning
+
+### Current Version
+
+- API Version: v1 (implicit, no version in URL)
+- Agent Version: 1.0.0
+
+### Compatibility Policy
+
+- **Breaking Changes**: Major version bump (v2)
+- **New Fields**: Added without version bump (agent ignores unknown fields)
+- **Deprecation**: 6-month notice before removal
+
+### Agent Behavior
+
+- Agent ignores unknown fields in server response
+- Agent sends only required fields in request
+- Agent handles missing optional fields gracefully
+
+## 6. Examples
+
+### Provisioning Flow
+
+```c
+// 1. Get hardware ID
+char hwid[32];
+get_hardware_id(hwid, sizeof(hwid));
+
+// 2. Send provisioning request
+char *response = http_post(server_url, "/api/v1/provision",
+ "{\"token\":\"%s\",\"hwid\":\"%s\"}", token, hwid);
+
+// 3. Parse encrypted config
+cJSON *json = cJSON_Parse(response);
+char *encrypted = cJSON_GetObjectItem(json, "encrypted_config")->valuestring;
+
+// 4. Decrypt config
+wg_config_t config;
+decrypt_config(encrypted, strlen(encrypted), hwid, salt, &config);
+
+// 5. Store in NVS
+config_save(&config);
+
+// 6. Apply to WireGuard
+tunnel_init(&config);
+```
+
+### Heartbeat Flow
+
+```c
+// 1. Get tunnel status
+bool tunnel_up = tunnel_is_up();
+int64_t last_hs = tunnel_get_last_handshake();
+
+// 2. Send heartbeat
+char *response = http_post(server_url, "/api/v1/heartbeat",
+ "{\"device_id\":\"%s\",\"tunnel_up\":%s,\"last_handshake\":\"%s\"}",
+ config.device_id, tunnel_up ? "true" : "false",
+ format_timestamp(last_hs));
+
+// 3. Check for config changes
+cJSON *json = cJSON_Parse(response);
+const char *new_hash = cJSON_GetObjectItem(json, "forwards_hash")->valuestring;
+
+if (strcmp(config.config_hash, new_hash) != 0) {
+ // Config changed, reload
+ load_config_from_response(json, &config);
+ tunnel_rebuild(&config);
+ config_save(&config);
+}
+```
+
+## 7. Error Handling
+
+### HTTP Retries
+
+```c
+#define MAX_RETRIES 3
+#define RETRY_DELAY_MS 1000
+
+for (int i = 0; i < MAX_RETRIES; i++) {
+ esp_err_t err = http_post(...);
+ if (err == ESP_OK) break;
+ vTaskDelay(RETRY_DELAY_MS * (i + 1) / portTICK_PERIOD_MS);
+}
+```
+
+### Timeout Settings
+
+- Connect timeout: 10 seconds
+- Read timeout: 30 seconds
+- Write timeout: 10 seconds
+
+## 8. Security Notes
+
+- All communication over HTTPS (TLS 1.2+)
+- Registration token is single-use (consumed on first provisioning)
+- Device can re-provision by obtaining new token
+- Keys never logged or transmitted in plaintext
diff --git a/reference/esp32-agent/docs/ARCHITECTURE.md b/reference/esp32-agent/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..1863039
--- /dev/null
+++ b/reference/esp32-agent/docs/ARCHITECTURE.md
@@ -0,0 +1,387 @@
+# ESP32 Agent — Module Architecture
+
+**Status**: ARCHITECTURE ONLY
+**Last Updated**: 2026-07-06
+
+## Overview
+
+This document defines the module architecture for the ESP32-based WireGuard agent. The agent runs on FreeRTOS + ESP-IDF and connects to NexusGuard Server Core via HTTP.
+
+## Task Architecture
+
+```
+FreeRTOS Tasks
+├── main_task # Initialization, event loop
+├── wifi_task # WiFi STA management (priority: 5)
+├── http_task # HTTP client (provisioning, heartbeat) (priority: 4)
+├── wg_task # WireGuard tunnel management (priority: 3)
+└── led_task # Status LED indication (priority: 1)
+```
+
+## State Machine
+
+```
+┌─────────────┐
+│ BOOT │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ WIFI_INIT │ ◄──────────────────────────────┐
+└──────┬──────┘ │
+ │ connected │
+ ▼ │
+┌─────────────┐ fail ┌─────────────┐│
+│ PROVISION │───────────────►│ WIFI_RETRY ││
+└──────┬──────┘ └──────┬──────┘│
+ │ success │ │
+ ▼ └───────┘
+┌─────────────┐
+│ TUNNEL_UP │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ HEARTBEAT │ ◄─── 30s interval
+└──────┬──────┘
+ │ config_changed
+ ▼
+┌─────────────┐
+│ TUNNEL_REBUILD │
+└──────┬──────┘
+ │
+ └──► HEARTBEAT
+```
+
+## Module Design
+
+### 1. main.c
+
+**Responsibilities**:
+- Hardware initialization (NVS, WiFi, GPIO)
+- Task creation and event loop
+- Signal handling (Ctrl+C graceful shutdown)
+
+**Key Functions**:
+```c
+void app_main(void); // Entry point
+void shutdown_handler(void); // Graceful shutdown
+```
+
+### 2. provision.c
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/provision`
+- Parse encrypted config response
+- Decrypt config via crypto.c
+- Store config in NVS
+
+**Key Functions**:
+```c
+esp_err_t provision_device(const char *server_url, const char *token,
+ const char *hwid, wg_config_t *config);
+esp_err_t decrypt_config(const uint8_t *encrypted, size_t len,
+ const char *hwid, const char *salt,
+ wg_config_t *config);
+```
+
+**HTTP Request**:
+```json
+POST /api/v1/provision
+Content-Type: application/json
+
+{
+ "token": "registration-token",
+ "hwid": "esp32-hardware-id"
+}
+```
+
+**HTTP Response**:
+```json
+{
+ "encrypted_config": "base64-encoded-bytes"
+}
+```
+
+### 3. heartbeat.c
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/heartbeat` every 30s
+- Send device status + tunnel state
+- Receive config sync (detect changes)
+- Trigger tunnel rebuild if config changed
+
+**Key Functions**:
+```c
+esp_err_t heartbeat_send(const wg_config_t *config,
+ const char *status, bool tunnel_up,
+ int64_t last_handshake);
+bool heartbeat_config_changed(const char *old_hash, const char *new_hash);
+```
+
+**HTTP Request**:
+```json
+POST /api/v1/heartbeat
+Content-Type: application/json
+
+{
+ "device_id": "uuid",
+ "status": "connected",
+ "tunnel_up": true,
+ "last_handshake": "2026-06-26T09:00:00Z"
+}
+```
+
+**HTTP Response**:
+```json
+{
+ "device_id": "uuid",
+ "status": "ok",
+ "internal_ip": "10.172.21.2",
+ "private_key": "hex",
+ "preshared_key": "hex",
+ "server_pub": "hex",
+ "endpoint": "italy-twenty.gl.at.ply.gg:59750",
+ "dns": "1.1.1.1",
+ "allowed_ips": "0.0.0.0/0",
+ "server_wg_ip": "10.172.21.1",
+ "forwards_hash": "hash-string"
+}
+```
+
+### 4. tunnel.c
+
+**Responsibilities**:
+- Initialize WireGuard tunnel via lwIP
+- Apply config (keys, endpoint, allowed IPs)
+- Monitor tunnel state (handshake time)
+- Rebuild tunnel on config change
+
+**Key Functions**:
+```c
+esp_err_t tunnel_init(const wg_config_t *config);
+esp_err_t tunnel_apply_config(const wg_config_t *config);
+esp_err_t tunnel_rebuild(const wg_config_t *config);
+int64_t tunnel_get_last_handshake(void);
+bool tunnel_is_up(void);
+```
+
+**WireGuard Config Application**:
+```c
+// Pseudo-code
+wg_device wg = {
+ .private_key = config->private_key,
+ .listen_port = 0, // ephemeral
+};
+wg_peer peer = {
+ .public_key = config->server_pub,
+ .preshared_key = config->preshared_key,
+ .endpoint = config->endpoint,
+ .allowed_ips = config->allowed_ips,
+};
+wg_set_device(&wg);
+wg_add_peer(&wg, &peer);
+wg_set_peer_allowed_ips(&wg, &peer, config->allowed_ips);
+```
+
+### 5. crypto.c
+
+**Responsibilities**:
+- Key derivation: SHA256(hwid + salt)
+- AES-256-GCM decryption
+- Secure memory handling
+
+**Key Functions**:
+```c
+esp_err_t crypto_derive_key(const char *hwid, const char *salt,
+ uint8_t key[32]);
+esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
+ const uint8_t key[32], uint8_t **plaintext,
+ size_t *plaintext_len);
+```
+
+**Algorithm**:
+```
+Key Derivation:
+ key = SHA256(hwid + salt) // 32 bytes
+
+Decryption:
+ nonce = ciphertext[0:12] // first 12 bytes
+ encrypted = ciphertext[12:] // rest
+ plaintext = AES-256-GCM-Decrypt(key, nonce, encrypted)
+```
+
+### 6. config.c
+
+**Responsibilities**:
+- NVS read/write for config fields
+- Config validation
+- Config versioning
+
+**Key Functions**:
+```c
+esp_err_t config_save(const wg_config_t *config);
+esp_err_t config_load(wg_config_t *config);
+esp_err_t config_clear(void);
+bool config_is_valid(const wg_config_t *config);
+```
+
+**NVS Keys**:
+| Key | Max Size | Description |
+|-----|----------|-------------|
+| `device_id` | 37 | UUID string |
+| `private_key` | 64 | WG private key hex |
+| `preshared_key` | 64 | PSK hex |
+| `internal_ip` | 18 | IP address |
+| `server_pub` | 64 | Server public key hex |
+| `endpoint` | 128 | Server endpoint |
+| `dns` | 64 | DNS server |
+| `allowed_ips` | 128 | Allowed IPs |
+| `config_hash` | 64 | Last config hash |
+
+### 7. wifi.c
+
+**Responsibilities**:
+- WiFi STA initialization
+- Connection management
+- Reconnection handling
+
+**Key Functions**:
+```c
+esp_err_t wifi_init_sta(const char *ssid, const char *password);
+esp_err_t wifi_connect(void);
+esp_err_t wifi_disconnect(void);
+bool wifi_is_connected(void);
+```
+
+## Data Structures
+
+### wg_config_t
+
+```c
+typedef struct {
+ char device_id[37]; // UUID
+ char private_key[64]; // WG private key hex
+ char preshared_key[64]; // PSK hex
+ char internal_ip[18]; // e.g. "10.172.21.2"
+ char server_pub[64]; // Server WG public key hex
+ char endpoint[128]; // e.g. "italy-twenty.gl.at.ply.gg:59750"
+ char dns[64]; // DNS server
+ char allowed_ips[128]; // e.g. "0.0.0.0/0"
+ char server_wg_ip[18]; // e.g. "10.172.21.1"
+ char config_hash[64]; // Last config hash
+} wg_config_t;
+```
+
+### device_state_t
+
+```c
+typedef enum {
+ STATE_BOOT,
+ STATE_WIFI_INIT,
+ STATE_WIFI_CONNECTED,
+ STATE_PROVISIONING,
+ STATE_PROVISIONED,
+ STATE_TUNNEL_UP,
+ STATE_TUNNEL_DOWN,
+ STATE_HEARTBEAT,
+ STATE_ERROR
+} device_state_t;
+```
+
+## Error Handling
+
+### Error Codes
+
+```c
+typedef enum {
+ ERR_OK = 0,
+ ERR_WIFI_CONNECT = -1,
+ ERR_HTTP_PROVISION = -2,
+ ERR_HTTP_HEARTBEAT = -3,
+ ERR_CRYPTO_DECRYPT = -4,
+ ERR_CONFIG_INVALID = -5,
+ ERR_TUNNEL_INIT = -6,
+ ERR_TUNNEL_APPLY = -7,
+ ERR_NVS_READ = -8,
+ ERR_NVS_WRITE = -9,
+} agent_error_t;
+```
+
+### Recovery Strategy
+
+| Error | Recovery |
+|-------|----------|
+| WiFi connect fail | Retry 3x, then reboot |
+| HTTP provision fail | Retry 3x, then reboot |
+| HTTP heartbeat fail | Log only, continue |
+| Crypto decrypt fail | Clear config, re-provision |
+| Tunnel init fail | Retry 3x, then reboot |
+| NVS read fail | Use defaults, re-provision |
+
+## Implementation Status
+
+### ✅ Implemented (Real Code)
+
+| Module | File | Status |
+|--------|------|--------|
+| Provisioning | `provision.c` | HTTP POST working |
+| Heartbeat | `heartbeat.c` | HTTP loop working |
+| Crypto | `crypto.c` | mbedtls AES-256-GCM |
+| Config | `config.c` | NVS read/write |
+| WiFi | `wifi.c` | STA connection |
+
+### ❌ STUB (Needs Implementation)
+
+| Module | File | What's Missing |
+|--------|------|----------------|
+| Tunnel | `tunnel.c` | `wireguard-esp32` component integration |
+| Heartbeat | `heartbeat.c` | Config sync parsing from response |
+
+### TODO: tunnel.c Integration
+
+When integrating `wireguard-esp32` component:
+
+```c
+// 1. Parse private_key hex to binary
+// 2. Parse server_pub hex to binary
+// 3. Parse preshared_key hex to binary
+// 4. Create WireGuard device with private_key
+// 5. Add peer with server_pub, preshared_key, endpoint
+// 6. Set peer allowed_ips
+// 7. Start WireGuard tunnel
+```
+
+### TODO: heartbeat.c Config Sync
+
+When parsing heartbeat response:
+
+```c
+// 1. Parse response JSON
+// 2. Extract config_hash
+// 3. Compare with stored config_hash
+// 4. If different → parse new config fields
+// 5. Save to NVS
+// 6. Call tunnel_rebuild()
+```
+
+## Future Enhancements
+
+### Phase 2
+- OTA firmware updates
+- Custom CA certificate support
+- Power optimization (deep sleep)
+- Multiple server support
+
+### Phase 3
+- Zephyr RTOS port
+- ESP32-S2 support
+- Ethernet (SPI) support
+- Bluetooth provisioning
+
+## References
+
+- [ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)
+- [WireGuard Protocol](https://www.wireguard.com/protocol/)
+- [lwIP Documentation](https://www.nongnu.org/lwip/)
+- [mbedtls Documentation](https://mbed-tls.readthedocs.io/)
diff --git a/reference/esp32-agent/docs/DESIGN.md b/reference/esp32-agent/docs/DESIGN.md
new file mode 100644
index 0000000..4c08474
--- /dev/null
+++ b/reference/esp32-agent/docs/DESIGN.md
@@ -0,0 +1,582 @@
+# Technical Design Document
+
+**Version**: 1.0.0
+**Date**: 2026-06-26
+**Status**: Draft
+
+## 1. Executive Summary
+
+NexusGuard Embedded Agent is an ESP32-based WireGuard client designed for IoT and edge devices. It provides zero-trust VPN tunneling with auto-provisioning and heartbeat monitoring, connecting to the existing NexusGuard Server Core infrastructure.
+
+## 2. Goals & Non-Goals
+
+### Goals
+- ESP32-based WireGuard tunnel (LW840X compatible)
+- Auto-provisioning via HTTP (same API as Go agent)
+- Heartbeat monitoring with config sync
+- NVS-based config persistence (encrypted)
+- FreeRTOS task-based architecture
+- Low power consumption (WiFi sleep modes)
+- **Independent build** — no dependency on main NexusGuard repo
+- **Server discovery** — configurable server URL + registration token
+
+### Non-Goals
+- Port forwarding (userspace agent only)
+- gRPC support (too heavy for ESP32)
+- Zephyr RTOS support (ESP-IDF uses FreeRTOS)
+- Multi-peer support (single tunnel only)
+- Kernel WireGuard (lwIP userspace only)
+
+## 3. Architecture
+
+### 3.1 System Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ ESP32 (FreeRTOS) │
+├─────────────────────────────────────────────────────────┤
+│ Application Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ provision│ │ heartbeat│ │ tunnel │ │ config │ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
+│ │ │ │ │ │
+│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
+│ │ crypto.c │ │
+│ │ AES-256-GCM decrypt (mbedtls) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Network Stack (lwIP) │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ WireGuard (lwIP socket API) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Hardware Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ WiFi │ │ NVS │ │ UART │ │ GPIO │ │
+│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 3.2 Task Architecture
+
+```
+FreeRTOS Tasks
+├── main_task # Initialization, event loop
+├── wifi_task # WiFi STA management (priority: 5)
+├── http_task # HTTP client (provisioning, heartbeat) (priority: 4)
+├── wg_task # WireGuard tunnel management (priority: 3)
+└── led_task # Status LED indication (priority: 1)
+```
+
+### 3.3 State Machine
+
+```
+┌─────────────┐
+│ BOOT │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ WIFI_INIT │ ◄──────────────────────────────┐
+└──────┬──────┘ │
+ │ connected │
+ ▼ │
+┌─────────────┐ fail ┌─────────────┐│
+│ PROVISION │───────────────►│ WIFI_RETRY ││
+└──────┬──────┘ └──────┬──────┘│
+ │ success │ │
+ ▼ └───────┘
+┌─────────────┐
+│ TUNNEL_UP │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ HEARTBEAT │ ◄─── 30s interval
+└──────┬──────┘
+ │ config_changed
+ ▼
+┌─────────────┐
+│ TUNNEL_REBUILD │
+└──────┬──────┘
+ │
+ └──► HEARTBEAT
+```
+
+## 4. Server Discovery & Provisioning
+
+### 4.1 Overview
+
+The embedded agent connects to NexusGuard Server Core via HTTP. The agent must be configured with:
+- **Server URL**: The API endpoint (e.g., `https://api-nexus.datadunia.com`)
+- **Registration Token**: Single-use token for initial provisioning
+- **Server Salt**: For AES-256-GCM key derivation (shared secret)
+
+### 4.2 Provisioning Flow
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ PROVISIONING FLOW │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ 1. Device boots │
+│ │ │
+│ ▼ │
+│ 2. Check NVS for saved config │
+│ │ │
+│ ├─► Config exists ──► Load config ──► Skip to TUNNEL │
+│ │ │
+│ └─► No config ──► Continue to provisioning │
+│ │
+│ 3. Connect to WiFi │
+│ │ │
+│ ▼ │
+│ 4. HTTP POST /api/v1/provision │
+│ │ Request: { "token": "...", "hwid": "..." } │
+│ │ │
+│ ▼ │
+│ 5. Server validates token, generates WireGuard keys │
+│ │ │
+│ ▼ │
+│ 6. Server encrypts config with AES-256-GCM │
+│ │ Key = SHA256(hwid + salt) │
+│ │ │
+│ ▼ │
+│ 7. Server responds with encrypted_config │
+│ │ │
+│ ▼ │
+│ 8. Agent decrypts config using mbedtls │
+│ │ │
+│ ▼ │
+│ 9. Store config in NVS │
+│ │ │
+│ ▼ │
+│ 10. Apply WireGuard tunnel │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 4.3 Server Configuration Sources
+
+The embedded agent can obtain server configuration from:
+
+| Source | Priority | Use Case |
+|--------|----------|----------|
+| NVS (saved config) | 1 | After initial provisioning |
+| sdkconfig (menuconfig) | 2 | Build-time configuration |
+| SmartConfig / BLE | 3 | WiFi provisioning (future) |
+| QR Code | 4 | Manual provisioning (future) |
+
+### 4.4 Build-Time Configuration
+
+Configure server URL and token via `idf.py menuconfig`:
+
+```
+Component config → NexusGuard Agent
+ ├── Server URL (NEXUS_SERVER_URL)
+ ├── Registration Token (NEXUS_REG_TOKEN)
+ ├── Server Salt (NEXUS_SERVER_SALT)
+ └── WiFi SSID (NEXUS_WIFI_SSID)
+ └── WiFi Password (NEXUS_WIFI_PASSWORD)
+```
+
+### 4.5 Runtime Configuration (NVS)
+
+After first provisioning, config is stored in NVS:
+
+| Key | Description |
+|-----|-------------|
+| `server_url` | Server API URL |
+| `device_id` | Assigned device UUID |
+| `private_key` | WireGuard private key |
+| `preshared_key` | Pre-shared key |
+| `internal_ip` | Device IP address |
+| `server_pub` | Server public key |
+| `endpoint` | Server WireGuard endpoint |
+| `dns` | DNS server |
+| `allowed_ips` | Allowed IPs |
+| `server_wg_ip` | Server WireGuard IP |
+| `config_hash` | Last config hash |
+
+### 4.6 Server API Contract
+
+The embedded agent uses the **same HTTP API** as the Go device-agent:
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/api/v1/provision` | POST | Initial key exchange |
+| `/api/v1/heartbeat` | POST | Periodic status + config sync |
+
+**Key**: The API contract is stable and documented in `docs/API_COMPAT.md`. The server does not need to know whether the client is Go agent or ESP32 agent — it treats them identically.
+
+### 4.7 Independence from Main Repo
+
+The embedded agent is **fully independent**:
+
+```
+nexus-agent-embedded/ # Standalone repo
+├── main/ # C source code
+├── components/ # ESP-IDF components
+├── docs/ # Documentation
+├── CMakeLists.txt # Build system
+└── sdkconfig.defaults # Configuration
+```
+
+**No dependency on**:
+- `server-core` (Go backend)
+- `device-agent` (Go agent)
+- `dashboard-ui` (Vue frontend)
+
+**Build anywhere** with ESP-IDF toolchain — no Go, no Node.js, no Docker.
+
+## 5. Module Design
+
+### 4.1 main.c
+
+**Responsibilities**:
+- Hardware initialization (NVS, WiFi, GPIO)
+- Task creation and event loop
+- Signal handling (Ctrl+C graceful shutdown)
+
+**Key Functions**:
+```c
+void app_main(void); // Entry point
+void shutdown_handler(void); // Graceful shutdown
+```
+
+### 4.2 provision.c
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/provision`
+- Parse encrypted config response
+- Decrypt config via crypto.c
+- Store config in NVS
+
+**Key Functions**:
+```c
+esp_err_t provision_device(const char *server_url, const char *token,
+ const char *hwid, wg_config_t *config);
+esp_err_t decrypt_config(const uint8_t *encrypted, size_t len,
+ const char *hwid, const char *salt,
+ wg_config_t *config);
+```
+
+**HTTP Request**:
+```json
+POST /api/v1/provision
+Content-Type: application/json
+
+{
+ "token": "registration-token",
+ "hwid": "esp32-hardware-id"
+}
+```
+
+**HTTP Response**:
+```json
+{
+ "encrypted_config": "base64-encoded-bytes"
+}
+```
+
+### 4.3 heartbeat.c
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/heartbeat` every 30s
+- Send device status + tunnel state
+- Receive config sync (detect changes)
+- Trigger tunnel rebuild if config changed
+
+**Key Functions**:
+```c
+esp_err_t heartbeat_send(const wg_config_t *config,
+ const char *status, bool tunnel_up,
+ int64_t last_handshake);
+bool heartbeat_config_changed(const char *old_hash, const char *new_hash);
+```
+
+**HTTP Request**:
+```json
+POST /api/v1/heartbeat
+Content-Type: application/json
+
+{
+ "device_id": "uuid",
+ "status": "connected",
+ "tunnel_up": true,
+ "last_handshake": "2026-06-26T09:00:00Z"
+}
+```
+
+**HTTP Response**:
+```json
+{
+ "device_id": "uuid",
+ "status": "ok",
+ "internal_ip": "10.172.21.2",
+ "private_key": "hex",
+ "preshared_key": "hex",
+ "server_pub": "hex",
+ "endpoint": "italy-twenty.gl.at.ply.gg:59750",
+ "dns": "1.1.1.1",
+ "allowed_ips": "0.0.0.0/0",
+ "server_wg_ip": "10.172.21.1",
+ "forwards_hash": "hash-string"
+}
+```
+
+### 4.4 tunnel.c
+
+**Responsibilities**:
+- Initialize WireGuard tunnel via lwIP
+- Apply config (keys, endpoint, allowed IPs)
+- Monitor tunnel state (handshake time)
+- Rebuild tunnel on config change
+
+**Key Functions**:
+```c
+esp_err_t tunnel_init(const wg_config_t *config);
+esp_err_t tunnel_apply_config(const wg_config_t *config);
+esp_err_t tunnel_rebuild(const wg_config_t *config);
+int64_t tunnel_get_last_handshake(void);
+bool tunnel_is_up(void);
+```
+
+**WireGuard Config Application**:
+```c
+// Pseudo-code
+wg_device wg = {
+ .private_key = config->private_key,
+ .listen_port = 0, // ephemeral
+};
+wg_peer peer = {
+ .public_key = config->server_pub,
+ .preshared_key = config->preshared_key,
+ .endpoint = config->endpoint,
+ .allowed_ips = config->allowed_ips,
+};
+wg_set_device(&wg);
+wg_add_peer(&wg, &peer);
+wg_set_peer_allowed_ips(&wg, &peer, config->allowed_ips);
+```
+
+### 4.5 crypto.c
+
+**Responsibilities**:
+- Key derivation: SHA256(hwid + salt)
+- AES-256-GCM decryption
+- Secure memory handling
+
+**Key Functions**:
+```c
+esp_err_t crypto_derive_key(const char *hwid, const char *salt,
+ uint8_t key[32]);
+esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
+ const uint8_t key[32], uint8_t **plaintext,
+ size_t *plaintext_len);
+```
+
+**Algorithm**:
+```
+Key Derivation:
+ key = SHA256(hwid + salt) // 32 bytes
+
+Decryption:
+ nonce = ciphertext[0:12] // first 12 bytes
+ encrypted = ciphertext[12:] // rest
+ plaintext = AES-256-GCM-Decrypt(key, nonce, encrypted)
+```
+
+### 4.6 config.c
+
+**Responsibilities**:
+- NVS read/write for config fields
+- Config validation
+- Config versioning
+
+**Key Functions**:
+```c
+esp_err_t config_save(const wg_config_t *config);
+esp_err_t config_load(wg_config_t *config);
+esp_err_t config_clear(void);
+bool config_is_valid(const wg_config_t *config);
+```
+
+**NVS Keys**:
+| Key | Max Size | Description |
+|-----|----------|-------------|
+| `device_id` | 37 | UUID string |
+| `private_key` | 64 | WG private key hex |
+| `preshared_key` | 64 | PSK hex |
+| `internal_ip` | 18 | IP address |
+| `server_pub` | 64 | Server public key hex |
+| `endpoint` | 128 | Server endpoint |
+| `dns` | 64 | DNS server |
+| `allowed_ips` | 128 | Allowed IPs |
+| `config_hash` | 64 | Last config hash |
+
+### 4.7 wifi.c
+
+**Responsibilities**:
+- WiFi STA initialization
+- Connection management
+- Reconnection handling
+
+**Key Functions**:
+```c
+esp_err_t wifi_init_sta(const char *ssid, const char *password);
+esp_err_t wifi_connect(void);
+esp_err_t wifi_disconnect(void);
+bool wifi_is_connected(void);
+```
+
+## 5. Data Structures
+
+### 5.1 wg_config_t
+
+```c
+typedef struct {
+ char device_id[37]; // UUID
+ char private_key[64]; // WG private key hex
+ char preshared_key[64]; // PSK hex
+ char internal_ip[18]; // e.g. "10.172.21.2"
+ char server_pub[64]; // Server WG public key hex
+ char endpoint[128]; // e.g. "italy-twenty.gl.at.ply.gg:59750"
+ char dns[64]; // DNS server
+ char allowed_ips[128]; // e.g. "0.0.0.0/0"
+ char server_wg_ip[18]; // e.g. "10.172.21.1"
+ char config_hash[64]; // Last config hash
+} wg_config_t;
+```
+
+### 5.2 device_state_t
+
+```c
+typedef enum {
+ STATE_BOOT,
+ STATE_WIFI_INIT,
+ STATE_WIFI_CONNECTED,
+ STATE_PROVISIONING,
+ STATE_PROVISIONED,
+ STATE_TUNNEL_UP,
+ STATE_TUNNEL_DOWN,
+ STATE_HEARTBEAT,
+ STATE_ERROR
+} device_state_t;
+```
+
+## 6. Error Handling
+
+### 6.1 Error Codes
+
+```c
+typedef enum {
+ ERR_OK = 0,
+ ERR_WIFI_CONNECT = -1,
+ ERR_HTTP_PROVISION = -2,
+ ERR_HTTP_HEARTBEAT = -3,
+ ERR_CRYPTO_DECRYPT = -4,
+ ERR_CONFIG_INVALID = -5,
+ ERR_TUNNEL_INIT = -6,
+ ERR_TUNNEL_APPLY = -7,
+ ERR_NVS_READ = -8,
+ ERR_NVS_WRITE = -9,
+} agent_error_t;
+```
+
+### 6.2 Recovery Strategy
+
+| Error | Recovery |
+|-------|----------|
+| WiFi connect fail | Retry 3x, then reboot |
+| HTTP provision fail | Retry 3x, then reboot |
+| HTTP heartbeat fail | Log only, continue |
+| Crypto decrypt fail | Clear config, re-provision |
+| Tunnel init fail | Retry 3x, then reboot |
+| NVS read fail | Use defaults, re-provision |
+
+## 7. Power Management
+
+### 7.1 WiFi Sleep
+
+```c
+// Enable WiFi sleep when tunnel is up
+esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
+
+// Disable sleep during provisioning/heartbeat
+esp_wifi_set_ps(WIFI_PS_NONE);
+```
+
+### 7.2 Light Sleep
+
+```c
+// Enter light sleep between heartbeats (30s)
+esp_light_sleep_start();
+
+// Wake on: WiFi event, GPIO interrupt, timer
+```
+
+## 8. Security Considerations
+
+### 8.1 Key Storage
+
+- WireGuard keys stored in NVS (flash, encrypted by NVS encryption)
+- Never log keys or tokens
+- Clear memory after use: `memset_s(key, 0, sizeof(key))`
+
+### 8.2 TLS
+
+- Server communication over HTTPS (TLS 1.2+)
+- Skip certificate verification for internal network (configurable)
+- Future: Support custom CA certificate
+
+### 8.3 Hardware ID
+
+- Derived from ESP32 eFuse MAC or custom serial
+- Used for key derivation (HWID + salt)
+- Never transmitted in plaintext
+
+## 9. Testing Strategy
+
+### 9.1 Unit Tests
+
+- Crypto: Decrypt known ciphertext with known key
+- Config: NVS read/write cycle
+- Heartbeat: Parse server response
+
+### 9.2 Integration Tests
+
+- Provision → Tunnel → Heartbeat cycle
+- Config change detection
+- Tunnel rebuild on config change
+
+### 9.3 Hardware Tests
+
+- ESP32-S3 DevKit flashing
+- WiFi connection stability
+- WireGuard handshake verification
+- Power consumption measurement
+
+## 10. Future Enhancements
+
+### 10.1 Phase 2
+
+- OTA firmware updates
+- Custom CA certificate support
+- Power optimization (deep sleep)
+- Multiple server support
+
+### 10.2 Phase 3
+
+- Zephyr RTOS port
+- ESP32-S2 support
+- Ethernet (SPI) support
+- Bluetooth provisioning
+
+## 11. References
+
+- [ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)
+- [WireGuard Protocol](https://www.wireguard.com/protocol/)
+- [lwIP Documentation](https://www.nongnu.org/lwip/)
+- [mbedtls Documentation](https://mbed-tls.readthedocs.io/)
+- [NexusGuard Server API](../../server-core/docs/)
diff --git a/reference/esp32-agent/docs/HARDWARE.md b/reference/esp32-agent/docs/HARDWARE.md
new file mode 100644
index 0000000..23a8820
--- /dev/null
+++ b/reference/esp32-agent/docs/HARDWARE.md
@@ -0,0 +1,330 @@
+# Hardware Reference
+
+**Version**: 1.0.0
+**Date**: 2026-06-26
+
+## 1. Target Hardware
+
+### 1.1 LW840X Module
+
+The LW840X is an ESP32-based WiFi module designed for IoT applications.
+
+| Specification | Value |
+|---------------|-------|
+| MCU | ESP32 (Xtensa LX7 dual-core) |
+| Clock | 240 MHz |
+| RAM | 520 KB SRAM |
+| Flash | 4 MB (external) |
+| WiFi | 802.11 b/g/n (2.4 GHz) |
+| Bluetooth | None (LW840X) |
+| GPIO | 20+ (depends on module) |
+| ADC | 18 channels (12-bit) |
+| Operating Temp | -40°C to +85°C |
+| Voltage | 3.0V to 3.6V |
+
+### 1.2 Pin Mapping
+
+```
+LW840X Module Pinout
+┌─────────────────────────────────────┐
+│ │
+│ 3V3 ─┐ │
+│ GND ─┤ │
+│ EN ─┤ │
+│ IO0 ─┤ Boot/Flash mode │
+│ IO1 ─┤ TX0 (UART0) │
+│ IO3 ─┤ RX0 (UART0) │
+│ IO4 ─┤ Status LED │
+│ IO5 ─┤ (reserved) │
+│ IO12 ─┤ (reserved) │
+│ IO13 ─┤ (reserved) │
+│ IO14 ─┤ (reserved) │
+│ IO15 ─┤ (reserved) │
+│ IO16 ─┤ (reserved) │
+│ IO17 ─┤ (reserved) │
+│ IO18 ─┤ (reserved) │
+│ IO19 ─┤ (reserved) │
+│ IO21 ─┤ (reserved) │
+│ IO22 ─┤ (reserved) │
+│ IO23 ─┤ (reserved) │
+│ IO25 ─┤ (reserved) │
+│ IO26 ─┤ (reserved) │
+│ IO27 ─┤ (reserved) │
+│ IO32 ─┤ (reserved) │
+│ IO33 ─┤ (reserved) │
+│ IO34 ─┤ (reserved) │
+│ IO35 ─┤ (reserved) │
+│ │
+└─────────────────────────────────────┘
+```
+
+### 1.3 Minimal Circuit
+
+```
+Power Supply:
+ 3.3V ──── LW840X 3V3
+ GND ──── LW840X GND
+
+Decoupling:
+ 100nF ceramic capacitor between 3V3 and GND (close to module)
+
+Boot/Flash:
+ IO0 ──── 10K pull-up to 3V3 (normal boot)
+ IO0 ──── GND (flash mode)
+
+Status LED:
+ IO4 ──── 220Ω ──── LED ──── GND
+
+UART (for debugging):
+ IO1 (TX0) ──── USB-UART RX
+ IO3 (RX0) ──── USB-UART TX
+ GND ──── USB-UART GND
+```
+
+## 2. Power Requirements
+
+### 2.1 Current Consumption
+
+| Mode | Current | Notes |
+|------|---------|-------|
+| Active (WiFi TX) | 130-170 mA | Transmitting data |
+| Active (WiFi RX) | 80-100 mA | Receiving data |
+| Modem Sleep | 15-20 mA | WiFi connected, low duty cycle |
+| Light Sleep | 0.8-1.5 mA | CPU paused, WiFi wake |
+| Deep Sleep | 5-10 µA | RTC only, wake on GPIO |
+| Off | 0 µA | No power |
+
+### 2.2 Power Supply Design
+
+```
+Recommended:
+ Input: 5V USB or 12V DC
+ Regulator: AMS1117-3.3 or similar LDO
+ Capacity: 500mA minimum
+
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
+ │ 5V USB │────▶│ LDO │────▶│ LW840X │
+ │ │ │ 3.3V │ │ │
+ └─────────┘ └─────────┘ └─────────┘
+ │
+ GND
+```
+
+### 2.3 Battery Operation
+
+For battery-powered applications:
+
+| Battery | Capacity | Runtime (Active) | Runtime (Sleep) |
+|---------|----------|------------------|-----------------|
+| CR2032 | 225 mAh | ~1.5 hours | ~2 years |
+| 18650 | 3400 mAh | ~20 hours | ~30 years |
+| LiPo 1000mAh | 1000 mAh | ~6 hours | ~10 years |
+
+**Note**: Deep sleep with periodic wake (e.g., every 5 minutes) is recommended for battery operation.
+
+## 3. Antenna Options
+
+### 3.1 PCB Trace Antenna
+
+- **Pros**: Low cost, compact
+- **Cons**: Lower gain, sensitive to placement
+- **Range**: 10-30m (indoor)
+- **Use Case**: Short-range, cost-sensitive
+
+### 3.2 U.FL Connector
+
+- **Pros**: Higher gain, flexible placement
+- **Cons**: Additional cost, larger size
+- **Range**: 50-100m (indoor)
+- **Use Case**: Longer range, industrial
+
+### 3.3 External Antenna
+
+- **Pros**: Best performance, directional options
+- **Cons**: Highest cost, largest size
+- **Range**: 100m+ (outdoor)
+- **Use Case**: Outdoor, long-range
+
+## 4. Development Board
+
+### 4.1 ESP32-DevKitC
+
+Recommended for development and prototyping.
+
+| Feature | Specification |
+|---------|---------------|
+| MCU | ESP32-WROOM-32 |
+| Flash | 4 MB |
+| RAM | 520 KB SRAM |
+| WiFi | 802.11 b/g/n |
+| Bluetooth | BT 4.2 + BLE |
+| USB | Micro-USB (CP2102) |
+| GPIO | 38 pins |
+| Price | ~$5-10 |
+
+### 4.2 Flashing
+
+```bash
+# Install ESP-IDF
+# https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/
+
+# Set target
+idf.py set-target esp32
+
+# Build
+idf.py build
+
+# Flash (auto-detect port)
+idf.py -p /dev/ttyUSB0 flash
+
+# Monitor
+idf.py -p /dev/ttyUSB0 monitor
+```
+
+### 4.3 Pin Connections (DevKit)
+
+```
+Status LED:
+ GPIO4 ──── 220Ω ──── LED ──── GND
+
+Debug UART (optional):
+ GPIO1 (TX0) ──── USB-UART RX
+ GPIO3 (RX0) ──── USB-UART TX
+ GND ──── USB-UART GND
+```
+
+## 5. Production Board
+
+### 5.1 Design Guidelines
+
+1. **Power**:
+ - Use 3.3V LDO with 500mA capacity
+ - Add 100nF ceramic capacitor close to 3V3 pin
+ - Add 10µF tantalum capacitor for bulk decoupling
+
+2. **Antenna**:
+ - Keep antenna area clear of copper pours
+ - Minimum 10mm clearance around antenna
+ - Use U.FL connector for external antenna
+
+3. **Layout**:
+ - Route UART traces away from antenna
+ - Keep crystal traces short
+ - Ground plane under module
+
+4. **Enclosure**:
+ - Use non-metallic enclosure (plastic)
+ - Ensure antenna is not shielded
+ - Provide mounting holes
+
+### 5.2 Bill of Materials (BOM)
+
+| Component | Quantity | Package | Notes |
+|-----------|----------|---------|-------|
+| LW840X | 1 | Module | ESP32-based |
+| AMS1117-3.3 | 1 | SOT-223 | 3.3V LDO |
+| 100nF | 2 | 0402 | Decoupling |
+| 10µF | 1 | 0805 | Bulk cap |
+| 220Ω | 1 | 0402 | LED resistor |
+| LED | 1 | 0603 | Status indicator |
+| U.FL | 1 | SMD | Antenna connector |
+| Header | 1 | 2.54mm | Debug UART |
+
+### 5.3 Schematic
+
+```
+ ┌─────────────────┐
+ │ LW840X │
+ 3.3V ────────────┤ 3V3 GND ├──── GND
+ │ │
+ IO4 ──── 220Ω ───┤ IO4 TX0 ├──── UART RX
+ └── LED ──┤ RX0 ├──── UART TX
+ GND │ │
+ │ EN ├──── 10K ──── 3.3V
+ │ │
+ │ IO0 ├──── 10K ──── 3.3V
+ │ │ (flash: GND)
+ └─────────────────┘
+```
+
+## 6. Testing & Validation
+
+### 6.1 Hardware Tests
+
+1. **Power-on Test**:
+ - Verify 3.3V at module pin
+ - Check current consumption (~50mA idle)
+ - Confirm LED blinks on boot
+
+2. **WiFi Test**:
+ - Scan for WiFi networks
+ - Connect to test AP
+ - Measure RSSI at distance
+
+3. **UART Test**:
+ - Send AT commands (if firmware supports)
+ - Verify debug output
+ - Check baud rate (115200)
+
+4. **Flash Test**:
+ - Write/read NVS data
+ - Verify flash size
+ - Test wear leveling
+
+### 6.2 Production Test
+
+1. **Functional Test**:
+ - Provision with test server
+ - Verify WireGuard handshake
+ - Check heartbeat response
+
+2. **Stress Test**:
+ - Run for 24+ hours
+ - Monitor memory leaks
+ - Verify reconnection after WiFi drop
+
+3. **Environmental Test**:
+ - Operating temperature range
+ - Humidity resistance
+ - Vibration resistance
+
+## 7. Troubleshooting
+
+### 7.1 Common Issues
+
+| Symptom | Cause | Solution |
+|---------|-------|----------|
+| No boot | IO0 held low | Remove flash jumper |
+| No WiFi | Antenna issue | Check antenna connection |
+| High current | WiFi always on | Use modem sleep |
+| Crash on boot | Stack overflow | Increase task stack size |
+| Provision fail | Token expired | Get new token from server |
+
+### 7.2 Debug Tools
+
+- **Serial Monitor**: `idf.py monitor`
+- **JTAG Debugger**: OpenOCD + GDB
+- **Logic Analyzer**: Saleae or similar
+- **Power Analyzer**: Monsoon or similar
+
+## 8. Compliance
+
+### 8.1 Certifications
+
+- **FCC**: Required for US market
+- **CE**: Required for EU market
+- **IC**: Required for Canada market
+- **TELEC**: Required for Japan market
+
+### 8.2 Testing
+
+- EMC testing (emissions and immunity)
+- SAR testing (if applicable)
+- Environmental testing (temperature, humidity)
+
+## 9. References
+
+- [ESP32 Hardware Design Guidelines](https://www.espressif.com/sites/default/files/documentation/esp32_hardware_design_guidelines_en.pdf)
+- [ESP32 Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf)
+- [LW840X Module Datasheet](link-to-datasheet)
+- [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/)
diff --git a/reference/ios-agent/README.md b/reference/ios-agent/README.md
new file mode 100644
index 0000000..bb990e0
--- /dev/null
+++ b/reference/ios-agent/README.md
@@ -0,0 +1,58 @@
+# iOS Agent — Architecture Reference
+
+**Status**: ARCHITECTURE ONLY (no code)
+**Location**: `reference/ios-agent/` (NOT a submodule)
+**Mirror**: Android Agent (`apps/android-agent/`)
+
+## Overview
+
+This directory contains the architecture design for an iOS WireGuard agent that mirrors the Android agent's architecture, UI design, colors, and heartbeat flow.
+
+## Why `reference/` not `apps/`?
+
+If iOS agent is implemented in the future, it will be added as `apps/ios-agent/`. This `reference/` directory stores the architecture docs separately to avoid conflicts.
+
+## Purpose
+
+- Document the iOS agent architecture (mirrors Android)
+- Define UI design specs (colors, layout, components)
+- API contract (shared with server-core)
+- Track implementation status and TODOs
+
+## Architecture Mirror: Android → iOS
+
+| Android Component | iOS Equivalent | Notes |
+|-------------------|----------------|-------|
+| `AgentService.kt` | `PacketTunnelProvider` | Foreground service → NetworkExtension |
+| `TunnelManager.kt` | `WireGuardAdapter` | VPNService → NEPacketTunnelProvider |
+| `Heartbeat.kt` | `HeartbeatService` | HTTP loop (same) |
+| `Provisioning.kt` | `ProvisioningService` | HTTP provisioning (same) |
+| `ConfigStorage.kt` | `KeychainStorage` | SharedPreferences → Keychain |
+| `Encryptor.kt` | `CryptoManager` | JCE → CryptoKit |
+| `MainActivity.kt` | `ContentView` | Activity → SwiftUI |
+| `SettingsActivity.kt` | `SettingsView` | Activity → SwiftUI |
+| `BootReceiver.kt` | `BGAppRefreshTask` | Boot broadcast → Background Tasks |
+| `LogBuffer.kt` | `LogStore` | In-memory → CoreData |
+
+## Files
+
+| File | Description |
+|------|-------------|
+| `docs/DESIGN.md` | Technical design + UI specs |
+| `docs/ARCHITECTURE.md` | Module architecture |
+| `docs/API_COMPAT.md` | Server API contract |
+| `docs/UI_DESIGN.md` | UI design specs (colors, layout) |
+
+## Quick Start
+
+When implementing:
+
+1. Create Xcode project: `ios-agent`
+2. Add NetworkExtension capability
+3. Use `wireguard-apple` library for WireGuard
+4. Follow architecture in `docs/ARCHITECTURE.md`
+5. Match UI design in `docs/UI_DESIGN.md`
+
+## License
+
+Proprietary - NexusGuard
diff --git a/reference/ios-agent/docs/API_COMPAT.md b/reference/ios-agent/docs/API_COMPAT.md
new file mode 100644
index 0000000..69435a3
--- /dev/null
+++ b/reference/ios-agent/docs/API_COMPAT.md
@@ -0,0 +1,365 @@
+# iOS Agent — API Compatibility Reference
+
+**Status**: API CONTRACT
+**Mirror**: Android Agent (`apps/android-agent/`)
+
+## Overview
+
+The iOS agent communicates with the server via two HTTP endpoints:
+
+1. **Provisioning**: `POST /api/v1/provision` — Initial key exchange
+2. **Heartbeat**: `POST /api/v1/heartbeat` — Periodic status + config sync
+
+## 1. Provisioning
+
+### Request
+
+```
+POST /api/v1/provision
+Content-Type: application/json
+```
+
+```json
+{
+ "token": "string (required)",
+ "hwid": "string (required)"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `token` | string | Yes | Registration token (single-use) |
+| `hwid` | string | Yes | Hardware ID (iOS identifierForVendor) |
+
+### Response (Success)
+
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+```
+
+```json
+{
+ "encrypted_config": "base64-encoded-bytes"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `encrypted_config` | string | AES-256-GCM encrypted ConfigPayload |
+
+### Response (Error)
+
+```json
+{
+ "error": "string"
+}
+```
+
+| Status | Error | Description |
+|--------|-------|-------------|
+| 400 | `"invalid token format"` | Token is not valid UUID |
+| 400 | `"token already used"` | Token was already consumed |
+| 400 | `"invalid hardware id"` | HWID is empty or invalid |
+| 403 | `"token expired"` | Token has expired (24h TTL) |
+| 500 | `"provisioning failed"` | Server internal error |
+
+### Decrypted ConfigPayload
+
+After decrypting `encrypted_config`, the agent receives:
+
+```json
+{
+ "device_id": "string (UUID)",
+ "private_key": "string (hex, 64 chars)",
+ "preshared_key": "string (hex, 64 chars)",
+ "internal_ip": "string (e.g. 10.172.21.2)",
+ "server_pub": "string (hex, 64 chars)",
+ "endpoint": "string (e.g. italy-twenty.gl.at.ply.gg:59750)",
+ "dns": "string (e.g. 1.1.1.1)",
+ "allowed_ips": "string (e.g. 0.0.0.0/0)",
+ "server_wg_ip": "string (e.g. 10.172.21.1)"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `device_id` | string | Device UUID (use in heartbeat) |
+| `private_key` | string | WireGuard private key (hex) |
+| `preshared_key` | string | Pre-shared key (hex) |
+| `internal_ip` | string | Device IP address |
+| `server_pub` | string | Server WireGuard public key (hex) |
+| `endpoint` | string | Server WireGuard endpoint |
+| `dns` | string | DNS server IP |
+| `allowed_ips` | string | Allowed IPs (e.g. "0.0.0.0/0" for full tunnel) |
+| `server_wg_ip` | string | Server WireGuard IP |
+
+## 2. Heartbeat
+
+### Request
+
+```
+POST /api/v1/heartbeat
+Content-Type: application/json
+```
+
+```json
+{
+ "device_id": "string (required)",
+ "status": "string (optional)",
+ "state": "string (optional)",
+ "tunnel_up": "boolean (optional)",
+ "last_handshake": "string (optional, ISO 8601)"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `device_id` | string | Yes | Device UUID |
+| `status` | string | No | "connected", "recovering", "stopped" |
+| `state` | string | No | Free-form state description |
+| `tunnel_up` | boolean | No | WireGuard tunnel status |
+| `last_handshake` | string | No | ISO 8601 timestamp of last handshake |
+
+### Response (Success)
+
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+```
+
+```json
+{
+ "device_id": "string",
+ "status": "ok",
+ "internal_ip": "string",
+ "private_key": "string (hex)",
+ "preshared_key": "string (hex)",
+ "server_pub": "string (hex)",
+ "endpoint": "string",
+ "server_wg_ip": "string",
+ "dns": "string",
+ "allowed_ips": "string",
+ "forwards_hash": "string"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `device_id` | string | Device UUID |
+| `status` | string | "ok" or error |
+| `internal_ip` | string | Device IP (may change) |
+| `private_key` | string | WireGuard private key (may rotate) |
+| `preshared_key` | string | Pre-shared key (may rotate) |
+| `server_pub` | string | Server public key |
+| `endpoint` | string | Server endpoint |
+| `server_wg_ip` | string | Server WireGuard IP |
+| `dns` | string | DNS server |
+| `allowed_ips` | string | Allowed IPs |
+| `forwards_hash` | string | Hash of port forwards (for change detection) |
+
+### Response (Error)
+
+```json
+{
+ "error": "string"
+}
+```
+
+| Status | Error | Description |
+|--------|-------|-------------|
+| 400 | `"invalid device id"` | Device ID is not valid UUID |
+| 403 | `"device is suspended"` | Device is suspended by admin |
+| 500 | `"heartbeat failed"` | Server internal error |
+
+## 3. Crypto Protocol
+
+### Key Derivation
+
+```
+key = SHA256(hwid + salt)
+```
+
+- `hwid`: Hardware ID (iOS `identifierForVendor`)
+- `salt`: Server's `SERVER_SALT` environment variable
+- Output: 32-byte AES key
+
+### Encryption (Server Side)
+
+```
+plaintext = ConfigPayload JSON
+nonce = random 12 bytes
+ciphertext = AES-256-GCM-Encrypt(key, nonce, plaintext)
+encrypted_config = nonce + ciphertext
+```
+
+### Decryption (Agent Side)
+
+```swift
+let combined = Data(base64Encoded: encryptedConfig)!
+let nonce = combined.prefix(12)
+let ciphertext = combined.dropFirst(12)
+
+let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext)
+let plaintext = try AES.GCM.open(sealedBox, using: key)
+let json = try JSONSerialization.jsonObject(with: plaintext)
+```
+
+### Implementation Notes
+
+- Use `CryptoKit` for AES-256-GCM
+- Use `CryptoKit` for SHA-256 key derivation
+- Nonce size: 12 bytes (fixed for GCM)
+- Tag size: 16 bytes (default for GCM)
+
+## 4. Config Change Detection
+
+### Heartbeat Hash Comparison
+
+The server returns `forwards_hash` in heartbeat response. Agent should:
+
+1. Store last received `forwards_hash` in UserDefaults
+2. Compare with current hash
+3. If different, trigger config reload
+
+```swift
+let lastHash = UserDefaults.standard.string(forKey: "config_hash") ?? ""
+
+if lastHash != response.forwardsHash {
+ // Config changed, rebuild tunnel
+ tunnelManager.rebuildTunnel(with: newConfig)
+ UserDefaults.standard.set(response.forwardsHash, forKey: "config_hash")
+}
+```
+
+## 5. Hardware ID
+
+### iOS identifierForVendor
+
+```swift
+import UIKit
+
+let hwid = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
+```
+
+### Alternative: Custom Serial
+
+```swift
+func getHardwareId() -> String {
+ // Use Keychain to persist custom HWID
+ if let savedHwid = KeychainManager.get(key: "hwid") {
+ return savedHwid
+ }
+
+ let newHwid = UUID().uuidString
+ KeychainManager.set(key: "hwid", value: newHwid)
+ return newHwid
+}
+```
+
+## 6. Versioning
+
+### Current Version
+
+- API Version: v1 (implicit, no version in URL)
+- Agent Version: 1.0.0
+
+### Compatibility Policy
+
+- **Breaking Changes**: Major version bump (v2)
+- **New Fields**: Added without version bump (agent ignores unknown fields)
+- **Deprecation**: 6-month notice before removal
+
+### Agent Behavior
+
+- Agent ignores unknown fields in server response
+- Agent sends only required fields in request
+- Agent handles missing optional fields gracefully
+
+## 7. Examples
+
+### Provisioning Flow
+
+```swift
+// 1. Get hardware ID
+let hwid = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
+
+// 2. Send provisioning request
+let body = ["token": token, "hwid": hwid]
+let request = URLRequest(url: URL(string: "\(serverUrl)/api/v1/provision")!)
+
+// 3. Parse encrypted config
+let encrypted = try JSONDecoder().decode([String: String].self, from: data)["encrypted_config"]!
+
+// 4. Decrypt config
+let config = try CryptoManager.decryptConfig(encrypted: encrypted, hwid: hwid, salt: salt)
+
+// 5. Store in Keychain
+KeychainStorage.save(config)
+
+// 6. Apply to WireGuard
+try await tunnelManager.startTunnel(config: config)
+```
+
+### Heartbeat Flow
+
+```swift
+// 1. Get tunnel status
+let tunnelUp = tunnelManager.isTunnelUp()
+let lastHandshake = tunnelManager.getLastHandshake()
+
+// 2. Send heartbeat
+let body: [String: Any] = [
+ "device_id": config.deviceId,
+ "tunnel_up": tunnelUp,
+ "last_handshake": lastHandshake?.iso8601String ?? "1970-01-01T00:00:00Z"
+]
+
+// 3. Check for config changes
+let response = try JSONDecoder().decode(HeartbeatResponse.self, from: data)
+
+if config.configHash != response.forwardsHash {
+ // Config changed, reload
+ let newConfig = try parseConfig(from: response)
+ tunnelManager.rebuildTunnel(with: newConfig)
+ KeychainStorage.save(newConfig)
+}
+```
+
+## 8. Error Handling
+
+### HTTP Retries
+
+```swift
+func sendRequest(url: URL, body: Data, maxRetries: Int = 3) async throws -> Data {
+ for attempt in 1...maxRetries {
+ do {
+ let (data, response) = try await URLSession.shared.data(for: request)
+ if let httpResponse = response as? HTTPURLResponse,
+ httpResponse.statusCode == 200 {
+ return data
+ }
+ } catch {
+ if attempt == maxRetries {
+ throw error
+ }
+ try await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000))
+ }
+ }
+ throw AgentError.heartbeatFailed("Max retries exceeded")
+}
+```
+
+### Timeout Settings
+
+- Connect timeout: 10 seconds
+- Read timeout: 30 seconds
+- Write timeout: 10 seconds
+
+## 9. Security Notes
+
+- All communication over HTTPS (TLS 1.2+)
+- Registration token is single-use (consumed on first provisioning)
+- Device can re-provision by obtaining new token
+- Keys never logged or transmitted in plaintext
+- WireGuard keys stored in iOS Keychain (hardware-backed)
diff --git a/reference/ios-agent/docs/ARCHITECTURE.md b/reference/ios-agent/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..c753742
--- /dev/null
+++ b/reference/ios-agent/docs/ARCHITECTURE.md
@@ -0,0 +1,474 @@
+# iOS Agent — Module Architecture
+
+**Status**: ARCHITECTURE ONLY
+**Mirror**: Android Agent (`apps/android-agent/`)
+
+## Overview
+
+This document defines the module architecture for the iOS WireGuard agent. The agent uses NetworkExtension (NEPacketTunnelProvider) for tunnel management and mirrors the Android agent's architecture.
+
+## System Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ iOS (SwiftUI) │
+├─────────────────────────────────────────────────────────┤
+│ App Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
+│ │ │ │ │ │
+│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
+│ │ AgentManager │ │
+│ │ (orchestrates all components) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Service Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
+│ │ │ │ │ │
+│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
+│ │ CryptoManager │ │
+│ │ AES-256-GCM (CryptoKit) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ NetworkExtension Layer │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ PacketTunnelProvider │ │
+│ │ (WireGuard tunnel via wireguard-apple) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Storage Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │
+│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+## Task Architecture
+
+```
+iOS Threads/Tasks
+├── Main Thread # UI updates
+├── AgentManager # Orchestration (async/await)
+├── HeartbeatTask # HTTP heartbeat (30s interval)
+├── TunnelManager # WireGuard tunnel management
+└── BackgroundTask # BGAppRefreshTask (background refresh)
+```
+
+## State Machine
+
+```
+┌─────────────┐
+│ BOOT │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ APP_INIT │ ◄──────────────────────────────┐
+└──────┬──────┘ │
+ │ loaded │
+ ▼ │
+┌─────────────┐ fail ┌─────────────┐│
+│ PROVISION │───────────────►│ APP_RETRY ││
+└──────┬──────┘ └──────┬──────┘│
+ │ success │ │
+ ▼ └───────┘
+┌─────────────┐
+│ TUNNEL_UP │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ HEARTBEAT │ ◄─── 30s interval
+└──────┬──────┘
+ │ config_changed
+ ▼
+┌─────────────┐
+│ TUNNEL_REBUILD │
+└──────┬──────┘
+ │
+ └──► HEARTBEAT
+```
+
+## Module Design
+
+### 1. AgentManager
+
+**Responsibilities**:
+- Orchestrate all agent components
+- Manage agent lifecycle (start/stop)
+- Handle state transitions
+
+**Key Properties**:
+```swift
+class AgentManager: ObservableObject {
+ @Published var state: AgentState = .disconnected
+ @Published var tunnelInfo: TunnelInfo?
+ @Published var logs: [LogEntry] = []
+
+ private let provisioning: ProvisioningService
+ private let heartbeat: HeartbeatService
+ private let tunnel: TunnelManager
+ private let config: KeychainStorage
+}
+```
+
+**Key Methods**:
+```swift
+func startAgent() async
+func stopAgent()
+func rebuildTunnel(with config: WireGuardConfig) async
+```
+
+### 2. PacketTunnelProvider
+
+**Responsibilities**:
+- Handle NetworkExtension tunnel lifecycle
+- Start/stop WireGuard tunnel
+- Report tunnel status
+
+**Key Methods**:
+```swift
+override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void)
+override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
+override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?)
+```
+
+**WireGuard Integration**:
+```swift
+// Uses wireguard-apple library
+let adapter = WireGuardAdapter(with: self) { logLevel, message in
+ Logger.log(level: logLevel, message: message)
+}
+
+adapter.start(tunnelConfiguration: tunnelConfig) { error in
+ if let error = error {
+ completionHandler(error)
+ } else {
+ completionHandler(nil)
+ }
+}
+```
+
+### 3. TunnelManager
+
+**Responsibilities**:
+- Build WireGuard config from stored keys
+- Start/stop tunnel via PacketTunnelProvider
+- Monitor tunnel state
+- Track last handshake time
+
+**Key Methods**:
+```swift
+func startTunnel(config: WireGuardConfig) async throws
+func stopTunnel()
+func isTunnelUp() -> Bool
+func getLastHandshake() -> Date?
+```
+
+**Config Build**:
+```swift
+func buildTunnelConfig(from config: WireGuardConfig) -> TunnelConfiguration {
+ let interface = InterfaceConfiguration(
+ privateKey: config.privateKey,
+ addresses: [config.internalIp],
+ dns: [config.dns]
+ )
+
+ let peer = PeerConfiguration(
+ publicKey: config.serverPub,
+ allowedIPs: config.allowedIps,
+ endpoint: config.endpoint,
+ preSharedKey: config.presharedKey,
+ persistentKeepAlive: 25
+ )
+
+ return TunnelConfiguration(interface: interface, peers: [peer])
+}
+```
+
+### 4. HeartbeatService
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/heartbeat` every 30s
+- Send device status + tunnel state
+- Receive config sync (detect changes)
+- Trigger tunnel rebuild if config changed
+
+**Key Methods**:
+```swift
+func startHeartbeatLoop()
+func stopHeartbeat()
+func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig?
+```
+
+**HTTP Request**:
+```json
+POST /api/v1/heartbeat
+Content-Type: application/json
+
+{
+ "device_id": "uuid",
+ "status": "connected",
+ "tunnel_up": true,
+ "last_handshake": "2026-06-26T09:00:00Z"
+}
+```
+
+**HTTP Response**:
+```json
+{
+ "device_id": "uuid",
+ "status": "ok",
+ "internal_ip": "10.172.21.2",
+ "private_key": "hex",
+ "preshared_key": "hex",
+ "server_pub": "hex",
+ "endpoint": "italy-twenty.gl.at.ply.gg:59750",
+ "dns": "1.1.1.1",
+ "allowed_ips": "0.0.0.0/0",
+ "server_wg_ip": "10.172.21.1",
+ "forwards_hash": "hash-string"
+}
+```
+
+### 5. ProvisioningService
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/provision`
+- Parse encrypted config response
+- Decrypt config via CryptoManager
+- Store config in Keychain
+
+**Key Methods**:
+```swift
+func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig?
+func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig?
+```
+
+**HTTP Request**:
+```json
+POST /api/v1/provision
+Content-Type: application/json
+
+{
+ "token": "registration-token",
+ "hwid": "ios-hardware-id"
+}
+```
+
+**HTTP Response**:
+```json
+{
+ "encrypted_config": "base64-encoded-bytes"
+}
+```
+
+### 6. CryptoManager
+
+**Responsibilities**:
+- Key derivation: SHA-256(hwid + salt)
+- AES-256-GCM decryption
+- Secure memory handling
+
+**Key Methods**:
+```swift
+func deriveKey(token: String, hwid: String) -> SymmetricKey
+func decrypt(encoded: String, key: SymmetricKey) -> Data?
+```
+
+**Algorithm**:
+```swift
+// Key Derivation
+let input = "\(token)\(hwid)".data(using: .utf8)!
+let hash = SHA256.hash(data: input)
+let key = SymmetricKey(data: hash)
+
+// Decryption
+let combined = Data(base64Encoded: encoded)!
+let nonce = combined.prefix(12)
+let ciphertext = combined.dropFirst(12)
+
+let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext)
+let plaintext = try AES.GCM.open(sealedBox, using: key)
+```
+
+### 7. KeychainStorage
+
+**Responsibilities**:
+- Store WireGuard keys in Keychain
+- Store agent config in UserDefaults
+- Secure key access
+
+**Key Methods**:
+```swift
+func saveConfig(_ config: AgentConfig)
+func loadConfig() -> AgentConfig?
+func saveWireGuardConfig(_ config: WireGuardConfig)
+func loadWireGuardConfig() -> WireGuardConfig?
+func clearAll()
+```
+
+**Storage Strategy**:
+- **Keychain**: WireGuard keys (private_key, preshared_key)
+- **UserDefaults**: Non-sensitive config (server_url, device_id, endpoint)
+- **File**: Log entries (optional)
+
+### 8. LogStore
+
+**Responsibilities**:
+- Buffer log entries in memory
+- Persist to CoreData (optional)
+- Provide log stream for UI
+
+**Key Methods**:
+```swift
+func add(level: LogLevel, tag: String, message: String)
+func clear()
+func getLogs() -> [LogEntry]
+```
+
+**Log Levels**:
+```swift
+enum LogLevel: String {
+ case debug = "DEBUG"
+ case info = "INFO"
+ case warn = "WARN"
+ case error = "ERROR"
+}
+```
+
+## Data Structures
+
+### WireGuardConfig
+
+```swift
+struct WireGuardConfig: Codable {
+ let deviceId: String
+ let privateKey: String
+ let presharedKey: String
+ let internalIp: String
+ let serverPub: String
+ let endpoint: String
+ let dns: String
+ let allowedIps: String
+ let serverWgIp: String
+ let configHash: String
+ let forwardsHash: String
+}
+```
+
+### AgentConfig
+
+```swift
+struct AgentConfig: Codable {
+ let serverUrl: String
+ let registrationToken: String
+ let isProvisioned: Bool
+}
+```
+
+### AgentState
+
+```swift
+enum AgentState: String {
+ case disconnected = "disconnected"
+ case connecting = "connecting"
+ case connected = "connected"
+ case failed = "failed"
+}
+```
+
+### LogEntry
+
+```swift
+struct LogEntry: Identifiable {
+ let id = UUID()
+ let timestamp: Date
+ let level: LogLevel
+ let tag: String
+ let message: String
+}
+```
+
+## Error Handling
+
+### Error Types
+
+```swift
+enum AgentError: Error {
+ case wifiNotConnected
+ case provisioningFailed(String)
+ case heartbeatFailed(String)
+ case cryptoDecryptionFailed
+ case configInvalid
+ case tunnelInitFailed
+ case keychainError(OSStatus)
+}
+```
+
+### Recovery Strategy
+
+| Error | Recovery |
+|-------|----------|
+| WiFi not connected | Retry 3x, then pause heartbeat |
+| HTTP provision fail | Retry 3x, then notify user |
+| HTTP heartbeat fail | Log only, continue |
+| Crypto decrypt fail | Clear config, re-provision |
+| Tunnel init fail | Retry 3x, then notify user |
+| Keychain error | Use UserDefaults fallback |
+
+## Background Tasks
+
+### BGAppRefreshTask
+
+iOS doesn't have boot receiver like Android. Use Background Tasks:
+
+```swift
+// Register task
+BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) {
+ task in
+ self.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
+}
+
+// Schedule task
+let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh")
+request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes
+try BGTaskScheduler.shared.submit(request)
+```
+
+### Background Modes
+
+Add to `Info.plist`:
+```xml
+UIBackgroundModes
+
+ fetch
+ processing
+ vpn-api
+
+```
+
+## Dependencies
+
+- **wireguard-apple**: WireGuard tunnel library
+- **CryptoKit**: AES-256-GCM encryption
+- **NetworkExtension**: VPN tunnel management
+- **BackgroundTasks**: Background refresh
+- **SwiftUI**: UI framework
+- **Combine**: Reactive state management
+
+## Android → iOS Mapping
+
+| Android | iOS | Notes |
+|---------|-----|-------|
+| `LifecycleService` | `PacketTunnelProvider` | Background service |
+| `startForeground()` | `NEPacketTunnelProvider` | Always-on tunnel |
+| `SharedPreferences` | `Keychain + UserDefaults` | Config storage |
+| `BroadcastReceiver` | `BGAppRefreshTask` | Background triggers |
+| `lifecycleScope` | `Task {}` | Coroutine/async |
+| `OkHttp` | `URLSession` | HTTP client |
+| `Base64` | `Data(base64Encoded:)` | Encoding |
+| `Log.d/i/w/e` | `os_log` or `print` | Logging |
diff --git a/reference/ios-agent/docs/DESIGN.md b/reference/ios-agent/docs/DESIGN.md
new file mode 100644
index 0000000..0d4f213
--- /dev/null
+++ b/reference/ios-agent/docs/DESIGN.md
@@ -0,0 +1,425 @@
+# iOS Agent — Technical Design Document
+
+**Version**: 1.0.0
+**Date**: 2026-07-06
+**Status**: Draft
+**Mirror**: Android Agent (`apps/android-agent/`)
+
+## 1. Executive Summary
+
+NexusGuard iOS Agent is an iOS-based WireGuard client for mobile devices. It mirrors the Android agent's architecture, UI design, and heartbeat flow, using iOS-native APIs (NetworkExtension, CryptoKit, Keychain).
+
+## 2. Goals & Non-Goals
+
+### Goals
+- iOS-based WireGuard tunnel (NetworkExtension)
+- Auto-provisioning via HTTP (same API as Android/ESP32)
+- Heartbeat monitoring with config sync (30s interval)
+- Keychain-based config storage (hardware-backed encryption)
+- SwiftUI UI matching Android agent design
+- Background operation via NetworkExtension
+
+### Non-Goals
+- gRPC support (not needed for mobile)
+- Port forwarding (userspace agent only)
+- Jailbroken device support
+- macOS/iPadOS support (iPhone only for v1)
+
+## 3. Architecture
+
+### 3.1 System Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ iOS (SwiftUI) │
+├─────────────────────────────────────────────────────────┤
+│ App Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
+│ │ │ │ │ │
+│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
+│ │ AgentManager │ │
+│ │ (orchestrates all components) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Service Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
+│ │ │ │ │ │
+│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
+│ │ CryptoManager │ │
+│ │ AES-256-GCM (CryptoKit) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ NetworkExtension Layer │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ PacketTunnelProvider │ │
+│ │ (WireGuard tunnel via wireguard-apple) │ │
+│ └──────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Storage Layer │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │
+│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 3.2 Task Architecture
+
+```
+iOS Threads/Tasks
+├── Main Thread # UI updates
+├── AgentManager # Orchestration (async/await)
+├── HeartbeatTask # HTTP heartbeat (30s interval)
+├── TunnelManager # WireGuard tunnel management
+└── BackgroundTask # BGAppRefreshTask (background refresh)
+```
+
+### 3.3 State Machine
+
+```
+┌─────────────┐
+│ BOOT │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ APP_INIT │ ◄──────────────────────────────┐
+└──────┬──────┘ │
+ │ loaded │
+ ▼ │
+┌─────────────┐ fail ┌─────────────┐│
+│ PROVISION │───────────────►│ APP_RETRY ││
+└──────┬──────┘ └──────┬──────┘│
+ │ success │ │
+ ▼ └───────┘
+┌─────────────┐
+│ TUNNEL_UP │
+└──────┬──────┘
+ │
+ ▼
+┌─────────────┐
+│ HEARTBEAT │ ◄─── 30s interval
+└──────┬──────┘
+ │ config_changed
+ ▼
+┌─────────────┐
+│ TUNNEL_REBUILD │
+└──────┬──────┘
+ │
+ └──► HEARTBEAT
+```
+
+## 4. Module Design
+
+### 4.1 AgentManager
+
+**Responsibilities**:
+- Orchestrate all agent components
+- Manage agent lifecycle (start/stop)
+- Handle state transitions
+
+**Key Properties**:
+```swift
+class AgentManager: ObservableObject {
+ @Published var state: AgentState = .disconnected
+ @Published var tunnelInfo: TunnelInfo?
+ @Published var logs: [LogEntry] = []
+
+ private let provisioning: ProvisioningService
+ private let heartbeat: HeartbeatService
+ private let tunnel: TunnelManager
+ private let config: KeychainStorage
+}
+```
+
+**Key Methods**:
+```swift
+func startAgent() async
+func stopAgent()
+func rebuildTunnel(with config: WireGuardConfig) async
+```
+
+### 4.2 PacketTunnelProvider
+
+**Responsibilities**:
+- Handle NetworkExtension tunnel lifecycle
+- Start/stop WireGuard tunnel
+- Report tunnel status
+
+**Key Methods**:
+```swift
+override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void)
+override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
+override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?)
+```
+
+### 4.3 TunnelManager
+
+**Responsibilities**:
+- Build WireGuard config from stored keys
+- Start/stop tunnel via PacketTunnelProvider
+- Monitor tunnel state
+- Track last handshake time
+
+**Key Methods**:
+```swift
+func startTunnel(config: WireGuardConfig) async throws
+func stopTunnel()
+func isTunnelUp() -> Bool
+func getLastHandshake() -> Date?
+```
+
+### 4.4 HeartbeatService
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/heartbeat` every 30s
+- Send device status + tunnel state
+- Receive config sync (detect changes)
+- Trigger tunnel rebuild if config changed
+
+**Key Methods**:
+```swift
+func startHeartbeatLoop()
+func stopHeartbeat()
+func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig?
+```
+
+### 4.5 ProvisioningService
+
+**Responsibilities**:
+- HTTP POST to `/api/v1/provision`
+- Parse encrypted config response
+- Decrypt config via CryptoManager
+- Store config in Keychain
+
+**Key Methods**:
+```swift
+func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig?
+func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig?
+```
+
+### 4.6 CryptoManager
+
+**Responsibilities**:
+- Key derivation: SHA-256(hwid + salt)
+- AES-256-GCM decryption
+- Secure memory handling
+
+**Key Methods**:
+```swift
+func deriveKey(token: String, hwid: String) -> SymmetricKey
+func decrypt(encoded: String, key: SymmetricKey) -> Data?
+```
+
+### 4.7 KeychainStorage
+
+**Responsibilities**:
+- Store WireGuard keys in Keychain
+- Store agent config in UserDefaults
+- Secure key access
+
+**Key Methods**:
+```swift
+func saveConfig(_ config: AgentConfig)
+func loadConfig() -> AgentConfig?
+func saveWireGuardConfig(_ config: WireGuardConfig)
+func loadWireGuardConfig() -> WireGuardConfig?
+func clearAll()
+```
+
+### 4.8 LogStore
+
+**Responsibilities**:
+- Buffer log entries in memory
+- Persist to CoreData (optional)
+- Provide log stream for UI
+
+**Key Methods**:
+```swift
+func add(level: LogLevel, tag: String, message: String)
+func clear()
+func getLogs() -> [LogEntry]
+```
+
+## 5. Data Structures
+
+### 5.1 WireGuardConfig
+
+```swift
+struct WireGuardConfig: Codable {
+ let deviceId: String
+ let privateKey: String
+ let presharedKey: String
+ let internalIp: String
+ let serverPub: String
+ let endpoint: String
+ let dns: String
+ let allowedIps: String
+ let serverWgIp: String
+ let configHash: String
+ let forwardsHash: String
+}
+```
+
+### 5.2 AgentConfig
+
+```swift
+struct AgentConfig: Codable {
+ let serverUrl: String
+ let registrationToken: String
+ let isProvisioned: Bool
+}
+```
+
+### 5.3 AgentState
+
+```swift
+enum AgentState: String {
+ case disconnected = "disconnected"
+ case connecting = "connecting"
+ case connected = "connected"
+ case failed = "failed"
+}
+```
+
+### 5.4 LogEntry
+
+```swift
+struct LogEntry: Identifiable {
+ let id = UUID()
+ let timestamp: Date
+ let level: LogLevel
+ let tag: String
+ let message: String
+}
+```
+
+## 6. Error Handling
+
+### 6.1 Error Types
+
+```swift
+enum AgentError: Error {
+ case wifiNotConnected
+ case provisioningFailed(String)
+ case heartbeatFailed(String)
+ case cryptoDecryptionFailed
+ case configInvalid
+ case tunnelInitFailed
+ case keychainError(OSStatus)
+}
+```
+
+### 6.2 Recovery Strategy
+
+| Error | Recovery |
+|-------|----------|
+| WiFi not connected | Retry 3x, then pause heartbeat |
+| HTTP provision fail | Retry 3x, then notify user |
+| HTTP heartbeat fail | Log only, continue |
+| Crypto decrypt fail | Clear config, re-provision |
+| Tunnel init fail | Retry 3x, then notify user |
+| Keychain error | Use UserDefaults fallback |
+
+## 7. Background Tasks
+
+### 7.1 BGAppRefreshTask
+
+iOS doesn't have boot receiver like Android. Use Background Tasks:
+
+```swift
+// Register task
+BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) {
+ task in
+ self.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
+}
+
+// Schedule task
+let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh")
+request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes
+try BGTaskScheduler.shared.submit(request)
+```
+
+### 7.2 Background Modes
+
+Add to `Info.plist`:
+```xml
+UIBackgroundModes
+
+ fetch
+ processing
+ vpn-api
+
+```
+
+## 8. Security Considerations
+
+### 8.1 Key Storage
+
+- WireGuard private keys stored in iOS Keychain (hardware-backed)
+- Non-sensitive config in UserDefaults
+- Never log keys or tokens
+
+### 8.2 TLS
+
+- Server communication over HTTPS (TLS 1.2+)
+- Use `URLSession` with default TLS settings
+- Certificate pinning (optional, future)
+
+### 8.3 Hardware ID
+
+- Use `identifierForVendor` for device identification
+- Persists across app reinstalls until device factory reset
+- Used for key derivation (HWID + salt)
+
+## 9. Testing Strategy
+
+### 9.1 Unit Tests
+
+- Crypto: Decrypt known ciphertext with known key
+- Config: Keychain read/write cycle
+- Heartbeat: Parse server response
+
+### 9.2 Integration Tests
+
+- Provision → Tunnel → Heartbeat cycle
+- Config change detection
+- Tunnel rebuild on config change
+
+### 9.3 UI Tests
+
+- VPN toggle interaction
+- Tab switching
+- Settings save/load
+
+### 9.4 Device Tests
+
+- iPhone 12+ running iOS 16+
+- NetworkExtension background operation
+- Keychain access in background
+
+## 10. Future Enhancements
+
+### 10.1 Phase 2
+
+- iPadOS support
+- Widget for quick status
+- Shortcuts integration
+- Siri commands
+
+### 10.2 Phase 3
+
+- macOS Catalyst support
+- Apple Watch companion
+- CarPlay integration
+
+## 11. References
+
+- [Apple NetworkExtension Documentation](https://developer.apple.com/documentation/networkextension)
+- [WireGuard for iOS](https://github.com/WireGuard/wireguard-apple)
+- [CryptoKit Documentation](https://developer.apple.com/cryptokit/)
+- [Background Tasks](https://developer.apple.com/documentation/backgroundtasks)
+- [NexusGuard Server API](../../server-core/docs/)
diff --git a/reference/ios-agent/docs/UI_DESIGN.md b/reference/ios-agent/docs/UI_DESIGN.md
new file mode 100644
index 0000000..72c860c
--- /dev/null
+++ b/reference/ios-agent/docs/UI_DESIGN.md
@@ -0,0 +1,506 @@
+# iOS Agent — UI Design Specs
+
+**Status**: DESIGN ONLY
+**Mirror**: Android Agent (`apps/android-agent/`)
+
+## Overview
+
+This document defines the UI design for the iOS WireGuard agent. It mirrors the Android agent's design, colors, layout, and user experience.
+
+## Color Palette
+
+### Status Colors
+
+| Name | Hex | RGB | Usage |
+|------|-----|-----|-------|
+| `status_disconnected` | `#FFB0B0B0` | (176, 176, 176) | Disconnected state |
+| `status_connecting` | `#FFFFD93D` | (255, 217, 61) | Connecting state |
+| `status_connected` | `#FF4CAF50` | (76, 175, 80) | Connected state |
+| `status_failed` | `#FFFF6B6B` | (255, 107, 107) | Failed state |
+
+### UI Colors
+
+| Name | Hex | RGB | Usage |
+|------|-----|-----|-------|
+| `card_bg` | `#FF1A1A2E` | (26, 26, 46) | Card background |
+| `card_bg_dark` | `#FF0F0F1A` | (15, 15, 26) | Badge background |
+| `text_primary` | `#FFFFFFFF` | (255, 255, 255) | Primary text |
+| `text_secondary` | `#FFB0B0B0` | (176, 176, 176) | Secondary text |
+| `accent_green` | `#FF4CAF50` | (76, 175, 80) | Accent/highlight |
+| `accent_blue` | `#FF2196F3` | (33, 150, 243) | Links/actions |
+| `border_color` | `#FF2A2A3E` | (42, 42, 62) | Card borders |
+
+### Log Colors
+
+| Level | Hex | RGB |
+|-------|-----|-----|
+| `DEBUG` | `#FF6B6B6B` | (107, 107, 107) |
+| `INFO` | `#FFB0B0B0` | (176, 176, 176) |
+| `WARN` | `#FFFFD93D` | (255, 217, 61) |
+| `ERROR` | `#FFFF6B6B` | (255, 107, 107) |
+
+## Color Definitions (Swift)
+
+```swift
+import SwiftUI
+
+extension Color {
+ // Status colors
+ static let statusDisconnected = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
+ static let statusConnecting = Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D
+ static let statusConnected = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50
+ static let statusFailed = Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B
+
+ // UI colors
+ static let cardBackground = Color(red: 0.10, green: 0.10, blue: 0.18) // #1A1A2E
+ static let cardBackgroundDark = Color(red: 0.06, green: 0.06, blue: 0.10) // #0F0F1A
+ static let textPrimary = Color.white // #FFFFFF
+ static let textSecondary = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
+ static let accentGreen = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50
+ static let accentBlue = Color(red: 0.13, green: 0.59, blue: 0.95) // #2196F3
+ static let borderColor = Color(red: 0.16, green: 0.16, blue: 0.24) // #2A2A3E
+}
+```
+
+## Layout Structure
+
+### Main Screen (ContentView)
+
+```
+┌─────────────────────────────────────┐
+│ NexusGuard [⚙️ Settings]│
+├─────────────────────────────────────┤
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ ● STATUS: Connected │ │
+│ │ │ │
+│ │ IP Address 10.172.21.2 │ │
+│ │ Admin Web https://... │ │
+│ │ Device ID abc-123-... │ │
+│ │ Handshake 14:32:05 │ │
+│ │ Allowed IPs 0.0.0.0/0 │ │
+│ │ Transport HTTPS │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ [Status] [Ports] [Log] │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ Port Forwards: 2 │ │
+│ │ TCP :8080 → 10.172.21.2:80│ │
+│ │ UDP :53 → 10.172.21.2:53│ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ [Copy Log] [Clear Log] │ │
+│ │ │ │
+│ │ 14:32:05 [INFO] Agent... │ │
+│ │ 14:32:05 [INFO] Tunnel... │ │
+│ │ 14:32:05 [WARN] Retry... │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ ══════════════════ │ │
+│ │ VPN Toggle │ │
+│ └─────────────────────────────┘ │
+│ │
+└─────────────────────────────────────┘
+```
+
+### Settings Screen (SettingsView)
+
+```
+┌─────────────────────────────────────┐
+│ ← Settings │
+├─────────────────────────────────────┤
+│ │
+│ Server URL │
+│ ┌─────────────────────────────┐ │
+│ │ https://api-nexus.datadunia.com│ │
+│ └─────────────────────────────┘ │
+│ │
+│ Registration Token │
+│ ┌─────────────────────────────┐ │
+│ │ •••••••••••••••• │ │
+│ └─────────────────────────────┘ │
+│ │
+│ Auto-Start on Boot │
+│ ┌─────────────────────────────┐ │
+│ │ [Toggle] │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ Save Settings │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ Export Config │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ Import Config │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ Paste Config │ │
+│ └─────────────────────────────┘ │
+│ │
+└─────────────────────────────────────┘
+```
+
+## Component Specs
+
+### Status Card
+
+```swift
+struct StatusCard: View {
+ let state: AgentState
+ let config: WireGuardConfig?
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ // Status indicator
+ HStack {
+ Circle()
+ .fill(statusColor)
+ .frame(width: 12, height: 12)
+ Text(statusText)
+ .font(.headline)
+ .foregroundColor(statusColor)
+ }
+
+ // Info fields
+ if state == .connected {
+ InfoRow(label: "IP Address", value: config?.internalIp ?? "-")
+ InfoRow(label: "Admin Web", value: "https://api-nexus.datadunia.com")
+ InfoRow(label: "Device ID", value: config?.deviceId ?? "-")
+ InfoRow(label: "Handshake", value: lastHandshake ?? "-")
+ InfoRow(label: "Allowed IPs", value: config?.allowedIps ?? "-")
+ InfoRow(label: "Transport", value: transport)
+ }
+ }
+ .padding()
+ .background(Color.cardBackground)
+ .cornerRadius(12)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(Color.borderColor, lineWidth: 1)
+ )
+ }
+
+ private var statusColor: Color {
+ switch state {
+ case .disconnected: return .statusDisconnected
+ case .connecting: return .statusConnecting
+ case .connected: return .statusConnected
+ case .failed: return .statusFailed
+ }
+ }
+}
+```
+
+### Info Row
+
+```swift
+struct InfoRow: View {
+ let label: String
+ let value: String
+
+ var body: some View {
+ HStack {
+ Text(label)
+ .font(.subheadline)
+ .foregroundColor(.textSecondary)
+ Spacer()
+ Text(value)
+ .font(.subheadline)
+ .foregroundColor(.textPrimary)
+ .lineLimit(1)
+ }
+ }
+}
+```
+
+### Port Forward Row
+
+```swift
+struct PortForwardRow: View {
+ let forward: PortForward
+
+ var body: some View {
+ HStack {
+ Text(forward.protocol.uppercased())
+ .font(.caption)
+ .fontWeight(.bold)
+ .foregroundColor(.cardBackgroundDark)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color.accentGreen)
+ .cornerRadius(4)
+
+ Text(":\(forward.publicPort)")
+ .font(.subheadline)
+ .foregroundColor(.textPrimary)
+ .font(.system(.body, design: .monospaced))
+
+ Spacer()
+
+ Text("\(forward.targetIp):\(forward.targetPort)")
+ .font(.subheadline)
+ .foregroundColor(.textSecondary)
+ .font(.system(.body, design: .monospaced))
+ }
+ }
+}
+```
+
+### Log Entry
+
+```swift
+struct LogEntryView: View {
+ let entry: LogEntry
+
+ var body: some View {
+ HStack(alignment: .top) {
+ Text(entry.timestamp, style: .time)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundColor(entry.level.color)
+
+ Text("[\(entry.level.rawValue)]")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundColor(entry.level.color)
+
+ Text("\(entry.tag): \(entry.message)")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundColor(entry.level.color)
+ }
+ }
+}
+
+extension LogLevel {
+ var color: Color {
+ switch self {
+ case .debug: return Color(red: 0.42, green: 0.42, blue: 0.42) // #6B6B6B
+ case .info: return Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
+ case .warn: return Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D
+ case .error: return Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B
+ }
+ }
+}
+```
+
+### VPN Toggle
+
+```swift
+struct VPNToggle: View {
+ @Binding var isOn: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ RoundedRectangle(cornerRadius: 25)
+ .fill(isOn ? Color.accentGreen : Color.statusDisconnected)
+ .frame(height: 50)
+ .overlay(
+ HStack {
+ Circle()
+ .fill(Color.white)
+ .frame(width: 40, height: 40)
+ .offset(x: isOn ? 40 : -40)
+ Spacer()
+ }
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+```
+
+## Tab Layout
+
+```swift
+struct TabBar: View {
+ @Binding var selectedTab: Int
+
+ var body: some View {
+ HStack(spacing: 0) {
+ TabButton(title: "Status", isSelected: selectedTab == 0) {
+ selectedTab = 0
+ }
+ TabButton(title: "Ports", isSelected: selectedTab == 1) {
+ selectedTab = 1
+ }
+ TabButton(title: "Log", isSelected: selectedTab == 2) {
+ selectedTab = 2
+ }
+ }
+ .background(Color.cardBackground)
+ .cornerRadius(8)
+ }
+}
+
+struct TabButton: View {
+ let title: String
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .font(.subheadline)
+ .foregroundColor(isSelected ? .textPrimary : .textSecondary)
+ .padding(.vertical, 12)
+ .frame(maxWidth: .infinity)
+ .background(isSelected ? Color.accentGreen : Color.clear)
+ .cornerRadius(8)
+ }
+ }
+}
+```
+
+## Typography
+
+### Font Sizes
+
+| Element | Size | Weight | Design |
+|---------|------|--------|--------|
+| Status text | 18pt | Semibold | Default |
+| Info label | 14pt | Regular | Default |
+| Info value | 14pt | Regular | Monospaced |
+| Tab text | 14pt | Medium | Default |
+| Log text | 12pt | Regular | Monospaced |
+| Badge text | 10pt | Bold | Default |
+
+### Font Definitions
+
+```swift
+extension Font {
+ static let statusText = Font.headline
+ static let infoLabel = Font.subheadline
+ static let infoValue = Font.subheadline.monospaced()
+ static let tabText = Font.subheadline.weight(.medium)
+ static let logText = Font.caption.monospaced()
+ static let badgeText = Font.caption2.bold()
+}
+```
+
+## Spacing
+
+| Element | Value |
+|---------|-------|
+| Card padding | 16pt |
+| Card corner radius | 12pt |
+| Card border width | 1pt |
+| Row spacing | 12pt |
+| Badge padding | 8pt horizontal, 4pt vertical |
+| Badge corner radius | 4pt |
+| Button height | 50pt |
+| Button corner radius | 25pt |
+
+## Animation
+
+### Status Change
+
+```swift
+withAnimation(.easeInOut(duration: 0.3)) {
+ state = .connected
+}
+```
+
+### Tab Switch
+
+```swift
+withAnimation(.easeInOut(duration: 0.2)) {
+ selectedTab = 1
+}
+```
+
+### VPN Toggle
+
+```swift
+withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
+ isVPNOn.toggle()
+}
+```
+
+## Haptic Feedback
+
+```swift
+// Success
+UIImpactFeedbackGenerator(style: .medium).impactOccurred()
+
+// Error
+UINotificationFeedbackGenerator().notificationOccurred(.error)
+
+// Selection
+UISelectionFeedbackGenerator().selectionChanged()
+```
+
+## Dark Mode
+
+iOS agent uses dark mode only (matches Android design). Force dark mode:
+
+```swift
+@main
+struct NexusGuardApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .preferredColorScheme(.dark)
+ }
+ }
+}
+```
+
+## Responsive Design
+
+### iPhone SE
+
+- Compact layout
+- Smaller font sizes
+- Reduced padding
+
+### iPhone 14/15
+
+- Standard layout
+- Default font sizes
+- Standard padding
+
+### iPhone 14/15 Pro Max
+
+- Expanded layout
+- Larger font sizes
+- Increased padding
+
+## Accessibility
+
+### VoiceOver
+
+```swift
+Text("Connected")
+ .accessibilityLabel("VPN Status: Connected")
+ .accessibilityHint("Double tap to toggle VPN")
+
+InfoRow(label: "IP Address", value: "10.172.21.2")
+ .accessibilityElement(children: .combine)
+ .accessibilityLabel("IP Address: 10.172.21.2")
+```
+
+### Dynamic Type
+
+```swift
+Text("Status")
+ .font(.headline)
+ .dynamicTypeSize(...DynamicTypeSize.accessibility2)
+```
+
+### Reduce Motion
+
+```swift
+@Environment(\.accessibilityReduceMotion) var reduceMotion
+
+withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) {
+ state = .connected
+}
+```