# WhatsApp · Settings (Manage)

**Portal:** Manage · **Routes:** `manage.messages.settings.*` · **Nav:** Messages → Settings → General

## What it does
Holds the **global AI auto-reply guards** — **stored encrypted in the database**. `.env` / `config/whatsapp.php` remain the **fallback**: anything left blank uses the file/env value, so this is additive and safe to roll out gradually.

> **The guard FORM is not on the Settings page (moved 2026-07-27).** It renders on **AI Automation → AI Setting** (`Ai/Index.vue` ← `AiProfilesController::index`, which ships the effective values as a `guards` prop) — every value there is about *how the AI is allowed to reply*, so it belongs beside the AI profiles it constrains. It still `PUT`s `manage.messages.settings.update` (`SettingsController::update`), which writes only `aiPaths()`. **Settings → General is now read-only**: the Cloud connection soft-test and the webhook endpoints to paste into Meta, plus pointers to where the guards live.

> **The broadcast guards are NOT on this page (moved 2026-07-12).** They live on the **Broadcasts → Settings** tab (`Broadcasts/Settings.vue` ← `BroadcastsController::settings/updateSettings`), reached from the Broadcasts sub-nav (Campaigns / Segments / Settings) — see [broadcast.md](/docs/modules_handbook/manage/messages/whatsapp/broadcast.md). Both forms write the **same** encrypted `whatsapp_settings` row, but each saves **only its own subtree**: `WhatsappSetting::aiPaths()` here, `broadcastPaths()` there (both filtered by prefix out of the single `CONFIG_PATHS` source of truth, and passed as the `$onlyPaths` argument of `WhatsappSettingRepository::save()`), so neither page ever clobbers the other's values.

> **The Meta Cloud API credentials + general defaults are NOT managed here — they are configured purely from `.env`** (`WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_BUSINESS_ACCOUNT_ID`, `WHATSAPP_PARTNER_BUSINESS_ID`, `WHATSAPP_GRAPH_VERSION`, `WHATSAPP_DEFAULT_COUNTRY`, and `WHATSAPP_ES_CONFIG_ID` for Embedded Signup). Per-number credentials also stay on each channel (`whatsapp_channels.provider_config`, encrypted) — and for a channel connected through **Embedded Signup** that is where its **customer business token** lives, which the driver prefers over the central `WHATSAPP_ACCESS_TOKEN` (see [coexistence.md](/docs/modules_handbook/manage/messages/whatsapp/coexistence.md)). The `cloud.*`, `graph_version` and `default_country` paths were deliberately dropped from the settings page + `WhatsappSetting::CONFIG_PATHS` (2026-07-01), mirroring the Bridge. `ai.auto.timezone` was dropped too — it **always follows the app timezone** (`APP_TIMEZONE`, resolved exactly as in `config/app.php`), so there is no separate WhatsApp timezone to keep in sync.

> **The QR Bridge / Baileys connection is likewise `.env`-only** (`BRIDGE_API_URL`, `BRIDGE_API_KEY`, `BRIDGE_WEBHOOK_TOKEN`, `BRIDGE_WEBHOOK_URL`), so the bridge and Laravel share one source of truth (the `wa-bridge/` sidecar reads the same root `.env`).

## How it works
- **One global row (`whatsapp_settings`), two forms.** `WhatsappSetting` (key model: uuid + blame) stores everything in a single **`config`** column cast **`encrypted:array`** + `$hidden` (the whole blob is encrypted at rest, mirroring `whatsapp_channels.provider_config`). Nesting mirrors `config/whatsapp.php` (`ai.auto.*`, `ai.handoff_keywords`, `broadcast.*`). `WhatsappSetting::CONFIG_PATHS` is the authoritative list of managed dot-paths (the `cloud.*`, `graph_version`, `default_country` **and** `bridge.*` paths are intentionally NOT in it — all env-only); **`aiPaths()` / `broadcastPaths()`** split it by prefix so **this** page saves only `ai.*` and the Broadcasts → Settings tab only `broadcast.*`. `SECRET_PATHS` is **empty** (the page holds no secrets).
- **DB-over-config merge (zero driver changes).** `Src\Whatsapp\Services\WhatsappSettings::apply()` is called once from **`AppServiceProvider::boot()`** (guarded by try/catch so a pre-migration / DB hiccup just falls back to env). For every set path it does `config(["whatsapp.{$path}" => $value])`, so every existing **`config('whatsapp.*')`** caller transparently gets the effective value with no code change: the DB-managed paths (`ai.*`, `broadcast.*` — read by `GenerateAiReply`, the broadcast pacer) resolve to the DB value, while the **Cloud credentials** (`cloud.*` — read by `CloudApiDriver`, the Cloud verify + signature check in `WhatsAppWebhookController`), the **general defaults** (`graph_version` — `CloudApiDriver`; `default_country` — `PhoneNormalizer`) and the **Bridge** paths (`bridge.*` — read by `BridgeDriver` / `BridgeGateway` / the bridge webhook) are not managed here and resolve straight from `.env`. A stored `null` means "not set here" (config/env default stands); a stored `false` / `0` / `[]` IS a deliberate override.
- **No secrets on the page.** With the Cloud credentials now `.env`-only, the page holds no write-only secrets and `SECRET_PATHS` is empty (the `SecretField.vue` partial was removed). Every managed field (the AI guards) simply overwrites on save — a blank clears it back to the env fallback.
- **Connection soft-test.** "Test connection" (`SettingsController@test`) does a Graph `GET /{phone_number_id}?fields=display_phone_number,quality_rating` with the **`.env`** Cloud API token + Phone Number ID (CA-bundle aware) and records the result on `verify_status` / `verify_message` / `verified_at` — never blocks saving (mirrors the Zoom settings pattern).
- **Thin + GUIDELINES.** `SettingsController` is thin (explicit flat-field → nested-`config` mapping in `update`, then `save($input, WhatsappSetting::aiPaths())`); all writes via `WhatsappSettingRepository` in `DB::transaction`; validation in `Settings\UpdateRequest` (the five `ai_*` fields only — every one nullable, so a blank clears back to the env fallback; numeric guards bounded). Admin-gated by the `manage` route middleware (`auth` + `admin`).

## What it manages
| Section | Fields |
|---|---|
| **AI guards** | `max_consecutive`, `daily_cap`, `cooldown_seconds`, `pause_on_quality_red`, `ai.handoff_keywords` (the guards from [ai_profile.md](/docs/modules_handbook/manage/messages/whatsapp/ai_profile.md) → *Auto-reply safety guards*; per-profile **active hours** stay on each profile; `ai.auto.timezone` is not managed here — it always follows the app timezone `APP_TIMEZONE`) |

> **Elsewhere:** the **broadcast guards** (`broadcast.throttle_per_minute`, `daily_business_initiated_cap`, `quiet_hours_start/end`, `auto_pause_on_quality_red`, `auto_pause_on_template_paused`, `contact_min_interval_hours`, `require_consent_marketing`, `marketing_cooldown_hours`, plus the QR/Bridge controls `allow_bridge` / `bridge_per_minute` / `bridge_daily_cap` / `bridge_window_start`/`_end` / `bridge_typing_simulation`) are on the **Broadcasts → Settings** tab — see [broadcast.md](/docs/modules_handbook/manage/messages/whatsapp/broadcast.md).

> **`.env`-only (not on this page):** the **Cloud API credentials** (`WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_BUSINESS_ACCOUNT_ID`, `WHATSAPP_PARTNER_BUSINESS_ID`), the **general defaults** (`WHATSAPP_GRAPH_VERSION`, `WHATSAPP_DEFAULT_COUNTRY`), the **Embedded Signup** configuration (`WHATSAPP_APP_ID`, `WHATSAPP_ES_CONFIG_ID`), and the **QR Bridge / Baileys** connection (`BRIDGE_API_URL` / `BRIDGE_API_KEY` / `BRIDGE_WEBHOOK_TOKEN` / `BRIDGE_WEBHOOK_URL` — plus `WHATSAPP_QR_BRIDGE_HIDDEN_EMAILS`, the accounts the QR provider is hidden from entirely, written for a Meta App Review login; see the channel-management bullet in [readMe.md](/docs/modules_handbook/manage/messages/whatsapp/readMe.md)). Per-number Cloud credentials also stay on each channel (`whatsapp_channels.provider_config` — including the per-WABA business token of an Embedded-Signup channel).

## Related files
- [src/Whatsapp/WhatsappSetting.php](/src/Whatsapp/WhatsappSetting.php) — the encrypted single-row model (`CONFIG_PATHS` + the **`aiPaths()` / `broadcastPaths()`** prefix split / `SECRET_PATHS` / `VERIFY_*` / `current()` / `lastFour()`).
- [src/Whatsapp/Repositories/WhatsappSettingRepository.php](/src/Whatsapp/Repositories/WhatsappSettingRepository.php) — secret-aware merge `save($input, $onlyPaths)` (the `$onlyPaths` argument is what keeps the two forms from clobbering each other) + `recordVerification()`.
- [src/Whatsapp/Services/WhatsappSettings.php](/src/Whatsapp/Services/WhatsappSettings.php) — `apply()` (DB→config merge), called from [app/Providers/AppServiceProvider.php](/app/Providers/AppServiceProvider.php).
- [app/Http/Controllers/Manage/Whatsapp/SettingsController.php](/app/Http/Controllers/Manage/Whatsapp/SettingsController.php) · [app/Http/Requests/Manage/Whatsapp/Settings/UpdateRequest.php](/app/Http/Requests/Manage/Whatsapp/Settings/UpdateRequest.php).
- [resources/js/Pages/Manage/Messages/Settings/Index.vue](/resources/js/Pages/Manage/Messages/Settings/Index.vue) — **read-only**: the Cloud connection soft-test + the webhook endpoints, and pointers to where each set of guards now lives. (The Cloud-credential `SecretField.vue` partial was removed when credentials moved to `.env`-only.)
- The AI-guard form: [resources/js/Pages/Manage/Messages/Ai/Index.vue](/resources/js/Pages/Manage/Messages/Ai/Index.vue) ← [AiProfilesController::index](/app/Http/Controllers/Manage/Whatsapp/AiProfilesController.php) (`guards` prop) → `PUT manage.messages.settings.update`.
- The broadcast half: [resources/js/Pages/Manage/Messages/Broadcasts/Settings.vue](/resources/js/Pages/Manage/Messages/Broadcasts/Settings.vue) ← [BroadcastsController::settings/updateSettings](/app/Http/Controllers/Manage/Whatsapp/BroadcastsController.php) ← [Broadcasts/SettingsRequest](/app/Http/Requests/Manage/Whatsapp/Broadcasts/SettingsRequest.php) — see [broadcast.md](/docs/modules_handbook/manage/messages/whatsapp/broadcast.md).
- [config/whatsapp.php](/config/whatsapp.php) — the fallback defaults this page overrides.
- [database/migrations/2026_06_29_000004_create_whatsapp_settings_table.php](/database/migrations/2026_06_29_000004_create_whatsapp_settings_table.php).
- Routes: `manage.messages.settings.*` ([routes/web.php](/routes/web.php)) · Nav: the Messages group in [resources/js/Layouts/ManageLayout.vue](/resources/js/Layouts/ManageLayout.vue).
