> ## Documentation Index
> Fetch the complete documentation index at: https://docs.awokenlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Avatars

> The initials / URL avatar picker, hostname allowlist, and per-character storage.

Two avatar sources ship out of the box. Each player picks which one they want from the in-game Settings panel; the choice persists **per-character** (not just per-license, so two characters on the same license can have distinct avatars).

| Source     | What it renders                                                                                | Server-side cost                                                     |
| ---------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `initials` | A coloured letter tile (first letter of the character's first name, hashed colour background). | None - rendered entirely client-side.                                |
| `url`      | An `<img>` tag pointing at the player's chosen URL. Hostname + extension allowlisted.          | One URL validation on save; image fetch happens in the player's CEF. |

```lua config.lua theme={"dark"}
avatars = {
    sources = {
        initials = true,    -- ON: shows in the picker
        url      = true,    -- ON: shows in the picker
    },
    url = {
        allowedHosts = {
            'i.imgur.com',
            'imgur.com',
            'cdn.discordapp.com',
            'media.discordapp.net',
        },
        allowedExtensions = { 'png', 'jpg', 'jpeg', 'webp' },
    },
},
```

## Sources toggle

```lua theme={"dark"}
sources = {
    initials = true,
    url      = true,
},
```

Setting either to `false` hides that source from the player's picker. Players currently using a disabled source fall back to `initials` on their next connect.

| Combination                    | Result                                                                                                      |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `initials = true, url = true`  | Player sees both options in the picker. Default.                                                            |
| `initials = true, url = false` | No URL avatars - only the letter tile. Privacy-focused servers.                                             |
| `initials = false, url = true` | Forces players to pick a URL. **Not recommended** - new players without a URL fall back to initials anyway. |
| Both `false`                   | Falls back to `initials`. The picker just shows that option as the only choice.                             |

<Info>
  New players default to `initials` (hardcoded server-side). They keep using the letter tile until they actively pick a URL in the Settings panel.
</Info>

## URL allowlist

The biggest reason this isn't a free-form text field: a player can otherwise paste any URL into the chat and force every other player's CEF to fetch it. That's a vector for tracking pixels, DDOS reflection, and IP-grabbing. The allowlist caps that.

```lua theme={"dark"}
url = {
    allowedHosts = {
        'i.imgur.com',
        'imgur.com',
        'cdn.discordapp.com',
        'media.discordapp.net',
    },
    allowedExtensions = { 'png', 'jpg', 'jpeg', 'webp' },
},
```

### Validation rules

A URL is accepted iff **all** are true:

1. Starts with `http://` or `https://`.
2. Hostname (just the host, no path) appears verbatim in `allowedHosts` (case-insensitive).
3. File extension appears verbatim in `allowedExtensions` (case-insensitive).
4. Total length ≤ 512 chars.

Failed validation surfaces an **inline red error** in the player's Settings panel under the URL field, plus a chat system warning. The avatar doesn't change.

| Error key                  | Trigger                                     |
| -------------------------- | ------------------------------------------- |
| `avatar_url_empty`         | Empty input.                                |
| `avatar_url_too_long`      | Over 512 chars.                             |
| `avatar_url_malformed`     | Not parseable as `http(s)://host/path.ext`. |
| `avatar_url_bad_host`      | Hostname not in `allowedHosts`.             |
| `avatar_url_bad_extension` | File extension not in `allowedExtensions`.  |

## Per-character storage

Avatars are saved keyed by the framework's character id (e.g. QBOX `citizenid`), not just the license. Two characters on the same license each have their own avatar.

```jsonc theme={"dark"}
// awoken_chat_settings.data (JSON blob for one player's license)
{
    "characterAvatars": {
        "QBX12345": { "source": "url", "url": "https://i.imgur.com/aaa.png" },
        "QBX67890": { "source": "initials" }
    },
    // ... other settings
}
```

When the player switches character mid-session:

* Bridge fires `qbx_core:server:onPlayerLoaded` (or equivalent).
* Server pushes the new character's avatar fields to the NUI via `settingsSaved`.
* Settings panel + every chat row updates live.

### Standalone fallback

On a standalone server (no framework, no character id), the per-character layer doesn't apply. Avatars save as flat `avatarSource` + `avatarUrl` keys on the settings row, applying license-wide. That's the legacy behaviour pre-multichar.

## Avatar in anonymous channels

For channels with `anonymous = true`:

* The server strips `avatarUrl` from the payload before broadcasting.
* The chat row renders a neutral **grey silhouette** tile (`fa-user-secret` icon) instead of the player's real avatar.
* Every recipient sees the same silhouette - the avatar tile is keyed to the channel, not the player.

The moment the player sends in a non-anonymous channel, their real avatar is back in the same chat session - no relog needed.

See [**Categories**](/resources/awoken-chat/configuration/categories) for the full anonymous flag behaviour.

## Avatar in Discord webhooks

When archiving messages to Discord:

* The author block's avatar uses the player's URL avatar (if set).
* Players using `initials` mode have no URL - the Discord embed shows a default Discord icon.

For anonymous channels with `anonymousInWebhook = true`, the avatar is omitted from the Discord embed too (matching the "Anonymous" label).
