---
title: Developer Broadcasts
---

# Developer Broadcasts

## What it is

**What it is:** a way for the Omniscio developers/admins to push an inbox card to users'
apps — everyone, or a simple group — without shipping a new build. A broadcast is
authored once in the cloud and every signed-in app picks it up and drops it into its
local inbox, where it behaves like any other inbox card.

## Where to find it

### Sending a broadcast (admins only)

Settings → **Broadcasts** (visible only to owner/admin accounts):

1. Write a **Title** and **Message** (markdown works). Optionally attach a **Link** (or a
   file, which is uploaded to Omniscio's cloud file-hosting and delivered as a link).
2. Pick the **audience**: _Everyone_, or _Specific groups_ — by platform (Windows / Mac /
   Linux), by tier (free / pro / team / enterprise), and/or an app-version range
   (e.g. "version 2.4 or older" to nudge people to update).
3. Choose expiry: auto-expire in 30 days (recommended) or never.
4. **Send** — you confirm the audience first (a fleet push has no instant undo).

You also get a list of recent broadcasts with a **Stop delivering** button (a _recall_ —
see below).

### Who can send

Only a signed-in **admin/owner** can author a broadcast. This is enforced in the app's
backend with a cryptographically-verified check — hiding the panel is not the only gate, a
non-admin simply cannot publish.

## How it behaves

### How delivery works

- Each signed-in app checks the cloud ~45 seconds after launch and then hourly.
- It matches itself against the audience (its app version, platform, and tier) and, for
  each broadcast it matches and hasn't seen before, drops a card into the local inbox.
- **Exactly-once:** a small local ledger records every broadcast the app has delivered, so
  a dismissed broadcast never reappears, and the card is never duplicated.
- **Recall:** if you switch a broadcast off (or it expires), every app that received it
  archives the card on its next check — not just "stops sending new ones."
- **Scheduling (optional start time):** you can set a **"schedule for later"** time so a broadcast
  goes live at a future moment instead of immediately — the inverse of expiry. It's stored in the
  cloud right away but isn't delivered to any inbox until its start time passes (on that app's next
  ~hourly check, so it's "next check after the start," not to-the-second). Scheduling controls
  **delivery timing, not secrecy** — the draft isn't hidden, just not delivered yet — and a start
  time must be before the expiry.
- Delivery does **not** require being signed in (anyone gets the "everyone" / version /
  platform audiences); a tier-targeted broadcast only reaches signed-in apps that match
  the tier.

### Embedded live page — a real web page right in the card (advanced)

Beyond text and a link, a broadcast can embed a **full live web page** — your own HTML, CSS,
JavaScript, and forms — that renders **inline** in the recipient's inbox card and is made to look
like a native part of the app (no visible frame, matches the app's colors and fonts, grows to fit
its content).

- **How** — first publish your page to **Omniscio Shares** (the same "publish" you already use for
  reports and dashboards), then compose a broadcast, choose **Embedded page**, and paste the Shares
  link. Only Shares links are accepted — arbitrary outside websites are refused (a safety + framing
  limit, so the app's content-security policy never has to be widened).
- **Safe by construction** — the page runs in the SAME sandbox your Shares pages already use: its
  code and forms work, but it is walled off from the app in an isolated frame and cannot read
  anyone's data, cookies, or session, or navigate the app. Raw HTML never touches the app itself.
- **Looks native, not a boxed frame** — include the small Omniscio embed snippet on your page and it
  auto-sizes (the card grows to fit, no inner scrollbar) and adopts the app's fonts and colors
  automatically. A page without the snippet still renders (a default height, your own styling).
- **Forms both ways** — a form on the page can post to **your own backend** (your endpoint, a Google
  Form, …) AND/OR report answers back to **Omniscio** (collected and counted in the Responses /
  Analytics tabs) by calling the snippet's submit helper.
- **Gated rollout** — the embedded-page content type is behind the `broadcast-embed` flag while it
  rolls out, and it honors the global broadcasts kill switch like every other broadcast.

### Surveys — ask a question, collect the answers (forms)

A broadcast can carry a short **survey** — one or more questions the recipient answers right on
the inbox card. Questions come in four types: **free text**, **single-choice** (pick one),
**multi-select** (pick several), and a **rating scale** (e.g. 1–5, with optional labels on each
end). Authored in the admin console, answered in the app, collected in the cloud, and viewed back
in the admin console.

- **Author** — in the admin console's Broadcasts compose, add questions of any type: text,
  single-choice or multi-select (each with its options), or a rating scale (choose the range —
  1–5 up to 1–10 — and optional labels for the low and high ends). Mark any question required. A
  broadcast that carries questions becomes a survey.
- **Answer** — a recipient fills in the card and submits. Choice and multi-select render as tappable
  pills, the scale as a row of rating buttons; a text answer's box **honors that person's
  Enter-to-send vs Ctrl+Enter preference**, and the whole card **matches their theme** (light/dark
  and accent color). Answering **requires sign-in** and the answer is **identity-stamped**. Only a
  targeted recipient may answer; a fleet _Everyone_ survey is answerable by any signed-in user (by
  design). Survey broadcasts are delivered signed-in-only.
- **Never lost** — the app records the answer locally first, then sends it to the cloud
  best-effort, so a network hiccup never loses it; re-answering overwrites (one answer per person).
- **View responses** — the admin console's **Responses** tab: pick a broadcast to see the response
  count, per-option tallies for single-choice and multi-select questions, the average and spread of
  a rating scale, and the list of free-text answers.
- **Campaign analytics (across ALL campaigns)** — the admin console's **Analytics** tab (a 4th sub-tab
  under Broadcasts) rolls the **delivered → opened → responded** funnel UP across every campaign in a
  chosen window (7/30/60/90 days): a fleet total, per-campaign summary rows (with open%/respond% and a
  survey-vs-plain marker), and an over-time trend. It's a read-only aggregation over the existing
  Firestore data (no new store), admin-only. Because read-receipts are off by default, **delivered/opened
  reflect only receipt-enabled installs** — the tab labels this, and **responded** is the complete metric.

Responses are private to admins (locked server-side by Firestore rules); no app ever learns who
else received or answered a survey.

- **Delivery funnel (read receipts)** — behind an **off-by-default** flag (`broadcast-receipts`),
  the app reports when a survey is **delivered** to a signed-in recipient's inbox and when they
  **open** the card, so the Responses tab can show a **delivered → opened → responded** funnel.
  Signed-in only (anonymous installs never report), best-effort (never delays anything), and
  nothing is tracked until the flag is deliberately enabled.
- **Opt-out (the privacy control that governs all of the above)** — a real Settings toggle,
  **"Receive developer messages & surveys"** (Settings → Inbox, **on by default**). Turning it off
  stops new broadcasts from being delivered to that device's inbox and stops any read-receipt from
  firing; if you're signed in, the choice is also written to your cloud profile so the server stops
  sending you targeted surveys and rejects any stray receipt/response. (Announcements on the public
  fleet stream and receipts are enforced by the app on that device; targeted surveys are enforced
  server-side. The setting is per-device — turning it off on one computer doesn't propagate the
  local switch to another, though a signed-in opt-out does stop targeted surveys everywhere.)
- **Anti-spam cap** — the app also caps how many broadcasts it will drop into an inbox per day
  (deferring any beyond the cap to later), so a runaway or misconfigured blast can never flood you.

### Reply — writing back without a survey

A survey only exists if you attached one when you sent the broadcast. Before this, a broadcast
sent *without* one was a one-way message: the card offered only the universal "Start session"
button, so a recipient who simply wanted to answer you had no way to. (That is exactly how this
was found — someone watched the welcome video, wanted to say thanks, and had to route around the
missing button to reach us.)

Every **announcement** card now carries a **Reply** button beside Start session: a delivered
broadcast, the welcome-video cards, and the one-time founding-team welcome note. It opens a small
box, and the reply comes back to us on the same channel a bug report uses — so it lands with the
team without you having to plan for it in advance.

- **The sender's address rides along**, prefilled from their signed-in account and editable. If
  they clear it (or aren't signed in) the box says plainly that we'll read the reply but won't be
  able to write back.
- **The reply says which card it answers**, so you know what they're responding to.
- **A reply that fails to send is never reported as sent** — if every route is down it is queued
  for retry and the sender is told to try again, not thanked.
- **A card that already shows a survey does NOT also show Reply.** One card offers one way to
  answer; otherwise the responses would split between the survey's own store and the feedback
  channel, and neither would be complete. So if you want the structured answers, attach the form —
  if you just want to hear back, send it plain and let them reply.

### Delivery & read receipts

When you send a **targeted** broadcast (to specific people, orgs, or groups — not a public
fleet-wide announcement), each recipient's app confirms back to the sender what happened to it, as a
three-step funnel — surfaced to the operator in the admin dashboard, **per recipient**:

- **Received** — the recipient's app fetched the broadcast from the cloud.
- **Presented** — it materialized into their inbox (shown as an ordinary card).
- **Read** — they opened the card.

This works for **every targeted content type** (text, link, embed, or survey/form), not only
surveys. Public/fleet-wide announcements are deliberately **not** tracked per person (that would mean
a write from every recipient, for little value).

**How it's reported (best-effort, privacy-respecting):**
- The app fires three tiny beacons — `received` (during the delivery poll), `delivered`/"Presented"
  (when the card is materialized), and `opened`/"Read" (fired once when the user opens the card, via
  the single selection-driven `useBroadcastReadReceipt` hook that covers all card types). Each is
  fire-and-forget: a failure never affects delivery or the user.
- Every beacon honors the **"Receive developer messages & surveys"** opt-out and the test/sandbox
  telemetry-suppression, is **signed-in only** (anonymous installs never phone home per-uid), and is
  bounded by a per-user daily quota. The cloud stores one `receipts/{uid}` doc per recipient
  (`receivedAt` / `deliveredAt` / `openedAt`, first-write-wins) that only the admin backend can read.
- The admin dashboard shows a **Received / Presented / Read** funnel per broadcast plus a **per-user
  drill-down** (who received / saw / opened it, and when).

**Honest limits:** receipts only come from installs on a build that has this instrumentation — an
older build never reports. The wire/storage names keep `delivered`/`opened` for back-compat with
already-shipped apps; the admin UI relabels them Presented/Read.

### Turning delivery off

Set the environment variable `AMC_DISABLE_BROADCASTS=1` to stop an install from receiving
broadcasts. It is on by default and intentionally has no user-facing toggle (broadcasts are
notices _to_ you, not a feature you opt into).

## For agents

### The automatic seam (for our own code)

The same engine is exposed as a main-process function, `publishBroadcast(...)`, so our own
code can fire a broadcast programmatically — e.g. when we detect a problem affecting a
group of users, push them a heads-up. (An agent-facing `POST /broadcast` CLI endpoint is a
planned follow-up.)

### Under the hood (for engineers)

Contract + invariants: [.claude/memory/contracts/broadcast-contract.md](../../.claude/memory/contracts/broadcast-contract.md).
A delivered broadcast is an ordinary `inbox_alert_items` row (`source_kind:'agent'`), so it
inherits the inbox primitive's render / dismiss / snooze / dedup behavior with no
special-casing.

## Related

Inbox cards in general, and the other kinds that land there, are covered by the inbox documentation; the email leg of Omniscio's outbound reports is a separate, per-install choice — see [Feedback channel opt-out](feedback-channel-opt-out.md).
