# VibeLive SDK -- Advanced API Reference

Version 0.86 | June 18, 2026

> This addendum covers features beyond the core video/audio flow documented in `API_GUIDE_v0.86.md`.
> All standard setup, lifecycle rules, and event wiring from that guide apply here too.

---

## Text Messaging

Persistent, database-backed text messages scoped to a channel or to a private member conversation.

### Key characteristics

- **Persistent by default** — messages are stored in the Makedo database. By default, they are retained after meeting end. Deletion on meeting end only occurs if `retainRoomTexts` / `retainDMTexts` is explicitly set to `false` at the room, project, or server level (see [Message Retention](#message-retention) below).
- **REST + real-time push** — send and history are REST (`sendMessage` / `getMessages`); incoming messages from *other* users are also pushed in real time over the SDK's WebSocket and surface via `setOnMessage()`.
- **Two scopes** — channel-wide (visible to all members) or private (between you and one member).
- **Separate from Galene SFU chat** — the WebRTC server has its own ephemeral in-session broadcast channel (exposed via `VibeLiveCore.setOnChatMessage()`). That channel is independent of these persistent DB messages and is not documented here.

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `sendMessage(channelId, content, memberId?)` | `Promise<Object>` | Send a text message. Omit `memberId` for channel-wide; provide it for a private message to that member. |
| `getMessages(channelId, options?)` | `Promise<Array>` | Fetch recent messages. Results are reverse-chronological (newest first). |
| `setOnMessage(callback)` | `void` | Register a handler for real-time incoming messages. Fires when a message is broadcast to the logged-in user (does NOT echo your own sends). |
| `deleteMessage(messageId)` | `Promise<Object>` | Soft-delete a single message. Only the message author may call this. |
| `deleteTextHistory(channelId?, options?)` | `Promise<Object>` | Soft-delete all message history for a channel. Room owner or system admin only. |

**`sendMessage(channelId, content, memberId?)`**

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `channelId` | `string` | Yes | The channel ID (not room code). Available as `VibeLive.channel?.id`. |
| `content` | `string` | Yes | The message text. |
| `memberId` | `string` | No | Omit for channel-wide message. Provide to send privately to one member. |

Returns the created message object (see shape below).

**`getMessages(channelId, options?)`**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `memberId` | `string` | `null` | Fetch private conversation with this member. Omit for channel-wide messages. |
| `count` | `number` | `10` | Number of messages to return. |
| `page` | `number` | `0` | Page offset (0-based) for pagination. |

Returns an array of message objects, newest first.

**`setOnMessage(callback)`**

| Param | Type | Description |
|-------|------|-------------|
| `callback` | `(message, wsIncoming) => void` | Called once per incoming message. `message` has the same shape as a `getMessages()` entry. `wsIncoming` is the raw WebSocket envelope, useful for debugging. |

The callback fires for:
- **Channel messages** sent by *other* members of any channel where you are an active member.
- **Private DMs** sent *to* you (`target_type:'member'`).

The callback does NOT fire for messages you sent yourself — use the resolved value of `sendMessage()` for sender-side updates.

Only one handler is registered at a time; calling `setOnMessage()` again replaces the previous handler.

**`deleteMessage(messageId)`**

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `messageId` | `string` | Yes | The `pid` field from a message object returned by `getMessages()` or `sendMessage()`. |

Soft-deletes the message. Only the message author may call this — the server rejects requests from any other user. Returns `{ success: true }` on success.

**`deleteTextHistory(channelId?, options?)`**

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `channelId` | `string` | No | The channel ID. Defaults to the current channel if omitted. |
| `options.roomMessages` | `boolean` | No | Delete channel-wide messages. Default `true`. |
| `options.dmMessages` | `boolean` | No | Also delete member-to-member DMs scoped to this room. Default `false`. |

Soft-deletes all matching messages in the channel. Requires the caller to be the room owner (creator) or a system admin — the server rejects requests from regular members. Returns `{ success: true }` on success.

> **Note:** Both delete operations are soft-deletes only (`deleted=true`). Rows remain in the database and can be recovered by a system administrator.

### Message object shape
  "content":            "Hello everyone",
  "content_type":       "text",
  "owner_id":           "U9876543210",
  "owner_username":     "alex",
  "sender_member_id":   "X1122334455",
  "sender_display_name": "Alex",
  "target_type":        "channel",
  "target_id":          "C1122334455",
  "uts":                1714406400000
}
```

| Field | Description |
|-------|-------------|
| `pid` | Unique message ID |
| `content` | Message text |
| `content_type` | Always `'text'` currently |
| `owner_id` | Sender's user ID (account-level, `U…` format) |
| `owner_username` | Sender's account username |
| `sender_member_id` | Sender's member ID in this channel (`X…` format). Use this for DM routing — it maps directly to the `memberId` used in `getMember()`, `registerTile()`, and `sendMessage()`. Reliable and stable; populated server-side from the sender's `ThingMember` row. |
| `sender_display_name` | Sender's per-channel display name. Prefer this over `owner_username` for display in chat UIs. |
| `target_type` | `'channel'` or `'member'` |
| `target_id` | The channel or member ID the message was addressed to |
| `uts` | Unix timestamp in milliseconds |

### Example — minimal chat panel

```js
const channelId = VibeLive.channel?.id;

// Send
async function sendChat(text) {
    if (!text.trim()) return;
    await VibeLive.sendMessage(channelId, text);
    await refreshMessages();
}

// Fetch and render (call after send, or on a poll interval)
async function refreshMessages() {
    const messages = await VibeLive.getMessages(channelId, { count: 20 });
    const chronological = [...messages].reverse(); // oldest first for display
    const box = document.getElementById('chat-box');
    box.innerHTML = chronological
        .map(m => `<p><strong>${m.owner_username}:</strong> ${escapeHtml(m.content)}</p>`)
        .join('');
    box.scrollTop = box.scrollHeight;
}

// Basic XSS guard
function escapeHtml(str) {
    return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
```

### Example — live chat with real-time updates

```js
const channelId = VibeLive.channel?.id;
const box = document.getElementById('chat-box');

// Initial history
const history = await VibeLive.getMessages(channelId, { count: 20 });
[...history].reverse().forEach(append);

// Live updates from other users
VibeLive.setOnMessage((msg) => {
    // Optional: filter to current channel only
    if (msg.target_type === 'channel' && msg.target_id !== channelId) return;
    append(msg);
});

// Sender side — append our own messages from the send result (no WS echo)
async function sendChat(text) {
    if (!text.trim()) return;
    const created = await VibeLive.sendMessage(channelId, text);
    append(created);
}

function append(m) {
    const p = document.createElement('p');
    p.innerHTML = `<strong>${escapeHtml(m.owner_username || m.owner_id)}:</strong> ${escapeHtml(m.content)}`;
    box.appendChild(p);
    box.scrollTop = box.scrollHeight;
}

function escapeHtml(str) {
    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
```

### Example — private message to a member

```js
// memberId available from VibeLive.getMemberIds() or a memberStateChange event
await VibeLive.sendMessage(channelId, 'Hey, just you', memberId);

// Fetch the private thread
const privateThread = await VibeLive.getMessages(channelId, { memberId, count: 20 });
```

### `createInstance()` usage

The same methods are available on independent instances:

```js
const room = VibeLive.createInstance({ projectId: '...' });
await room.signup('Alex');
await room.enterByRoomCode('ABC123');
await room.sendMessage(room.channel.id, 'Hello');
const msgs = await room.getMessages(room.channel.id);
```

### Notes

- `channelId` is `VibeLive.channel?.id` — available after `channelSelected` fires.
- Real-time push requires an authenticated WebSocket. The SDK starts the dispatcher automatically after login/signup, so as long as you're logged in, `setOnMessage` will fire.
- The author of a message does not receive a WS echo. Append your own sends from the resolved `sendMessage()` value.
- Private messages are scoped to the intersection of `channelId` + `memberId`. The same two users in different channels have separate threads.
- `content` is stored and returned as-is. Escape output before inserting into the DOM (see `escapeHtml` above).

---

### Message Retention

By default, messages are **retained** after a meeting ends. Deletion on meeting end only occurs when `retainRoomTexts` or `retainDMTexts` is explicitly set to `false`. Both flags follow the same cascade as `statusOnMeetingEnd` (room config → project config → system default `true`).

| Config key | Type | Default | Description |
|---|---|---|---|
| `retainRoomTexts` | `"true"` / `"false"` | `"true"` | Preserve channel-wide messages after meeting end. When `false`, all `target_type:'channel'` messages for the room are soft-deleted on meeting end. |
| `retainDMTexts` | `"true"` / `"false"` | `"true"` | Preserve private member DMs after meeting end. When `false`, all `target_type:'member'` DMs scoped to the room's members are soft-deleted on meeting end. |

To enable deletion, set either flag to `false` at the desired level of the cascade. For example, to delete all messages for a specific room at meeting end:

```js
// When creating a room (room-level override)
const room = await VibeLive.createRoom('Standup', { retainRoomTexts: 'false', retainDMTexts: 'false' });
```

Or set either flag at the project level to apply it across all rooms in the project.

> **Note:** Deletion is a soft-delete only (`deleted=true`). Rows remain in the database and can be recovered by a system administrator.

---

## Server-side sender fields

To populate per-channel sender labels, each message view object returned by
`getMessages()` and delivered via `setOnMessage` includes:

| Field | Type | Description |
|-------|------|-------------|
| `sender_display_name` | `string \| null` | The author's `display_name` resolved from their `member` row in the channel that owns this message. `null` when unresolvable. |
| `sender_member_id` | `string \| null` | The author's member ID in that channel. |

These are computed at view time (i.e. they reflect the *current* `display_name`, not the value at send time). Both `getMessages()` results and real-time `setOnMessage` payloads include them.

---

## Network Quality Monitoring (opt-in)

The SDK can sample each active WebRTC peer connection on a fixed interval, classify the result into one of four levels (`good` / `fair` / `poor` / `lost`) with a *reason*, and surface that either as data events or as an automatic top-edge status bar on the affected tile.

### Key characteristics

- **Opt-in** — disabled by default. Activated via `init({ features: { … } })`.
- **Local observation only** — everything is computed from your browser's `RTCPeerConnection.getStats()`. No server traffic, no per-peer messages. Phase 1 reflects what *your* side sees about each link (incoming audio loss, jitter, RTT, freezes, etc.).
- **Hysteresis** — a degraded level is only emitted after two consecutive degraded samples; recovery requires three consecutive good samples. This prevents UI flapping during transient hiccups.
- **Scoped to channel lifetime** — the monitor starts on `enterByChannelId` and stops on `leaveChannel` (or `#resetChannelState`).
- **Two surfaces** — pure data (callbacks + getters) and an optional auto-rendered tile bar.

### Feature flags

Pass any of these inside `features` on `init()`:

```js
VibeLive.init({
  projectId: 'demo-001',
  features: {
    connectionQuality: true,   // turn the monitor on (data + events)
    showMemberQuality: true,   // auto-render the bar on remote camera tiles
    showLocalQuality:  true,   // auto-render the bar on your own camera tile
  }
});
```

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `connectionQuality` | `boolean` | `false` | Master switch. Enables sampling and the `connectionQuality` / `memberQuality` events plus the getter methods. |
| `showMemberQuality` | `boolean` | `false` | Auto-render a status bar on each remote camera tile when its level drops below `good`. Implies `connectionQuality:true`. |
| `showLocalQuality` | `boolean` | `false` | Auto-render the same status bar on your own camera tile, driven by the overall (your-side) snapshot. Implies `connectionQuality:true`. |

You can enable `connectionQuality` alone and build your own UX. Or enable one of the `show*` flags for instant default UX; the data events still fire so you can layer additional behaviour on top.

### Events

| Event | Callback shape | When |
|-------|----------------|------|
| `connectionQuality` | `(snapshot) => void` | The overall (your-side) level transitions to a new value, after hysteresis. |
| `memberQuality` | `(memberId, snapshot) => void` | A specific remote member's downstream level transitions, after hysteresis. Fires once per change per member. |

Register either via the unified event API or via direct callback assignment:

```js
VibeLive.on('connectionQuality', (s) => console.log('overall:', s.level, s.reason));
VibeLive.on('memberQuality',     (id, s) => console.log(id, '→', s.level, s.reason));

// Or the legacy style
VibeLive.onConnectionQuality = (s) => { /* ... */ };
VibeLive.onMemberQuality     = (id, s) => { /* ... */ };
```

### Snapshot shape

```js
{
  level:    'good' | 'fair' | 'poor' | 'lost',
  reason:   string | null,        // see Reason vocabulary below
  metrics: {
    rttMs:         Number | null, // round-trip time, ms
    jitterMs:      Number | null, // playout jitter, ms
    packetLossPct: Number | null, // combined loss, %
    audioLossPct:  Number | null, // incoming-audio loss, %
    videoLossPct:  Number | null, // incoming-video loss, %
    outboundKbps:  Number | null, // your upload rate, kbps (overall only)
    freezeRate:    Number | null, // freezes per sample
    bytesGrowing:  boolean | null, // false → stalled
  },
  sampledAt: Number              // ms epoch when last sampled
}
```

For `memberQuality`, the snapshot reflects the *worst* of any active peer connections to that member (camera + screenshare aggregated).

### Reason vocabulary

Embedders may pattern-match on `snapshot.reason` for custom UX. All values are kebab-case strings exported as constants on `connection-quality.js#REASONS`:

| Reason | Meaning |
|--------|---------|
| `rtt` | Round-trip time elevated. |
| `jitter` | Playout jitter elevated. |
| `audio-loss` | Incoming-audio packet loss elevated. |
| `video-loss` | Incoming-video packet loss elevated. |
| `video-freeze` | Decoder reported frame freezes. |
| `upstream-loss` | Loss reported on your outbound stream. |
| `bitrate-low` | Your outbound bitrate is below the threshold. |
| `stalled` | No bytes received for the stall window (`stallMs`, default 8 s). |
| `recovered` | Level returned to `good`. |

### Getter methods

For synchronous snapshot access (e.g. to read current quality on demand, not via the event callbacks):

| Method | Returns | Description |
|--------|---------|-------------|
| `getConnectionQuality()` | `snapshot \| null` | Current overall snapshot, or `null` if the monitor is not running. |
| `getMemberQuality(memberId)` | `snapshot \| null` | Current per-member snapshot, or `null` if unknown / monitor off. |

Available on both the top-level `VibeLive` singleton and on instances returned from `createInstance()`.

### Automatic tile bar (showMemberQuality / showLocalQuality)

When enabled, the SDK injects a `<div class="makedo-quality-bar">` along the top edge of the affected camera tile. The bar:

- Appears only when `level !== 'good'`; auto-removes on recovery.
- Picks colour from level — amber (`fair`), orange (`poor`), red (`lost`).
- Picks text from `reason`, with self-vs-other phrasing for `showLocalQuality` (e.g. *"Your audio is breaking up"* vs *"Audio is breaking up"*).
- Carries `data-quality-level` and `data-quality-reason` attributes for CSS overrides.
- Renders at `z-index: 20` — above the watermark (10), below the tile DM panel (30).
- Is idempotent — if `registerTile` is called again, any current state is re-applied.

The bar attaches to the element you passed to `registerTile(memberId, 'camera', element)`. Apps that build their own tile DOM still own creation; the SDK only decorates registered tiles.

### Defaults and tuning

Sampling and thresholds are set inside `connection-quality.js` and are not yet surfaced as init options. Current defaults:

| Setting | Default | Notes |
|---------|---------|-------|
| `sampleIntervalMs` | `2000` | One stats poll every 2 s per active PC. |
| `degradeSamplesNeeded` | `2` | Consecutive bad samples to drop a level. |
| `recoverSamplesNeeded` | `3` | Consecutive good samples to recover. |
| `rttMs` thresholds | `{fair:150, poor:300, lost:800}` | Round-trip time. |
| `packetLossPct` thresholds | `{fair:1, poor:3, lost:10}` | Combined loss percentages. |
| `jitterMs` thresholds | `{fair:30, poor:60, lost:200}` | Playout jitter. |
| `stallMs` | `8000` | Bytes-received freeze window before a `stalled` reason. |

### Example — custom UX with events

```js
VibeLive.init({ projectId, features: { connectionQuality: true } });

VibeLive.on('memberQuality', (memberId, snap) => {
  const tile = document.querySelector(`[data-member-id="${memberId}"]`);
  if (!tile) return;
  tile.dataset.quality = snap.level;           // good | fair | poor | lost
  tile.dataset.reason  = snap.reason || '';
});

VibeLive.on('connectionQuality', (snap) => {
  document.body.dataset.netQuality = snap.level;
});
```

### Example — default bars, no app code

```js
VibeLive.init({
  projectId,
  features: { showMemberQuality: true, showLocalQuality: true }
});

// ...register tiles as usual:
VibeLive.registerTile(memberId, 'camera', tileEl);
```

That's it — bars appear and disappear automatically as quality changes.

### Notes

- The monitor reads from `RTCPeerConnection.getStats()`; if a browser does not expose a particular field (e.g. `freezeCount` in older Safari), the corresponding metric is `null` and that signal is silently skipped.
- "Alice can't hear *you*" is not knowable from your local stats alone in Phase 1 — only "Alice's audio reaching you is degraded". A future phase will add WS-level ping/pong and outbound-quality echo for round-trip awareness.
- The bar overlay is purely inline DOM/CSS; there is no external stylesheet to load. Override appearance by querying `.makedo-quality-bar` and setting your own properties, or replace text by listening to the events and updating `bar.textContent` yourself.
- Watermark and tile DM UX are unaffected by these flags.


