# Messaging credentials (Shared · `Src\Common\MessagingCredential`)

**Context:** Cross-cutting delivery configuration · **Service:** `Src\Common\Services\MessagingCredentialProvider` · **Managed from:** Messages → Settings → **Delivery APIs** (`/manage/integrations/messaging`) · **Consumed by:** [SMS](/docs/modules_handbook/shared/sms/readMe.md) (`Sms360Sender`), [Email](/docs/modules_handbook/shared/email/readMe.md) (the app mailer + `EmailSender` binding), GetResponse.

## What it does

The messaging **delivery credentials** — the SMS360 gateway login, the outgoing SMTP transport +
from-identity, and the GetResponse API key — stored as **one row per (type, provider)** with the
provider's own key set as an **encrypted JSON blob** (the `payment_gateways` shape: adding a
provider is a catalogue entry, never a schema change). An admin rotates them from the UI instead
of editing `.env`. Every value resolves **DB-first with `.env` fallback**: existing `.env`
setups keep working until a value is saved in the database, and a cleared `.env` keeps working
once it is.

Secrets are **encrypted at rest** (`encrypted:array` cast on `credentials`), hidden from
serialization, and surfaced to the settings UI only via per-field
[`credentialHints()`](/src/Common/MessagingCredential.php) — a non-secret field echoes its
value, a secret only a `set` flag + its last 4. A saved secret is never echoed back.

## How it works

- **One row per provider** — `messaging_credentials` columns: `type`
  (`TYPE_SMS` / `TYPE_EMAIL` / **`TYPE_VOICE`**), `provider` (`sms360` / `smtp` / `getresponse` /
  **`twilio`**, unique per type), optional `label`, the encrypted `credentials` blob,
  `is_enabled`, **`is_default`** (which provider a type resolves to — this is how the funnel
  email driver is picked, replacing the old `email_driver` column), and `verified_at`. The
  **`MessagingCredential::PROVIDERS` catalogue** declares each provider's type, name and field
  spec (`label` + `secret` flag per field); the **masking** (`credentialHints()`) and the
  repository's write rules derive from it.
  - ⚠️ **The catalogue does NOT generate the form.** `MessagingSettingsController` maps flat
    request fields to each provider by hand (`sms_user` → `sms360.user`, …), the `UpdateRequest`
    lists them explicitly, and the Vue page renders one hand-written `<section>` per provider.
    Adding a provider therefore needs **all four** touched, not just the catalogue entry — the
    2026-08-07 Twilio addition shipped with a catalogue entry alone and had no way to be typed
    in until the form caught up. `credentialHints()` still forces a secret field's `value` to
    `null`, so whatever the page renders can never carry a saved secret.
- Writes go through
  [`MessagingCredentialRepository::save()`](/src/Common/Repositories/MessagingCredentialRepository.php)
  inside a transaction: non-secret fields overwrite (a present blank clears), **secrets are
  write-only** (a blank submission keeps the stored value), and a type's first row auto-becomes
  its default. `markDefault()` promotes a provider within its type, demoting siblings.
- **`applyOverrides()` is the one consumer-facing hook.** Called from
  `AppServiceProvider::boot()`, it pushes stored values INTO the runtime config
  (`services.sms360.*`, `mail.mailers.smtp.*` + `mail.from.*` — the PRIMARY mailer of the
  failover chain only, never the `.env`-driven `backup` — `services.funnel_email.driver`,
  `services.getresponse.api_key`, **`services.twilio.*`**) — so every existing reader
  (`Sms360Sender`, the `Mail` facade, the `EmailSender` binding, `TwilioVoiceCaller`) picks them
  up **without knowing this class exists**.
  Best-effort: before the table exists (fresh clone, mid-deploy) it silently does nothing.
- **A DB value wins; an unsaved field leaves `.env` untouched.** Decryption failures (e.g. a
  rotated `APP_KEY`) are logged and treated as unset rather than thrown.

## Reference usage

**Consumers should NOT call this service.** Read your credentials from `config(...)` exactly as
before (`services.sms360.user`, `mail.mailers.smtp.host`, …) — the provider has already overridden them at
boot. That indirection is the whole point: no feature couples to where credentials live.

The only direct callers are:

- [`AppServiceProvider::boot()`](/app/Providers/AppServiceProvider.php) — `applyOverrides()` at boot.
- [`MessagingSettingsController`](/app/Http/Controllers/Manage/Integrations/MessagingSettingsController.php)
  — the Delivery APIs page: flattens the rows' `credentialHints()` into the form's field shape,
  writes via the repository, and offers a throttled test-send per channel.

## Related files

- [src/Common/MessagingCredential.php](/src/Common/MessagingCredential.php) — the model (`TYPES`/`PROVIDERS` catalogue, encrypted `credentials` blob, `credentialHints()`).
- [src/Common/Services/MessagingCredentialProvider.php](/src/Common/Services/MessagingCredentialProvider.php) — DB-first resolution + `applyOverrides()`.
- [src/Common/Repositories/MessagingCredentialRepository.php](/src/Common/Repositories/MessagingCredentialRepository.php) — the transactional singleton write.
- [app/Http/Controllers/Manage/Integrations/MessagingSettingsController.php](/app/Http/Controllers/Manage/Integrations/MessagingSettingsController.php) — the Delivery APIs settings page.
- [app/Http/Requests/Manage/Integrations/Messaging/UpdateRequest.php](/app/Http/Requests/Manage/Integrations/Messaging/UpdateRequest.php) — validation.
- [database/migrations/2026_07_30_120001_generalise_messaging_credentials.php](/database/migrations/2026_07_30_120001_generalise_messaging_credentials.php) — the (type, provider, credentials) table (replaced the original wide singleton).

**See also:** [SMS](/docs/modules_handbook/shared/sms/readMe.md) · [Email](/docs/modules_handbook/shared/email/readMe.md) · [Funnel automation](/docs/modules_handbook/manage/events/funnel-automation/readMe.md) (the feature that motivated DB-backed credentials).
