danro-talk

Async web chat widget that bridges website visitors to your team's Zulip or Discord — with Japanese and Korean UI, designed for community sites that can't (or don't want to) be online 24/7.

Drop one <script> and one <danro-talk> element into a page; replies show up in your team chat as a topic / thread. Your team replies there, the visitor sees it in the widget. No third-party SaaS, no per-seat pricing.

<!-- Zulip-backed widget (ws-url auto-detected when loaded from the same Worker) -->
<script type="module" src="https://danro-api.atfedi.de/widget.js"></script>
<danro-talk site-id="zl_xxxxxxxxxxxx" lang="ja"></danro-talk>

<!-- Discord-backed widget -->
<danro-talk site-id="dc_xxxxxxxxxxxx" lang="ko" target="discord"></danro-talk>

What's in the box

Why

Existing chat widgets (Intercom, Crisp, etc.) assume an agent is always on. For a small community site — a fediverse instance, a personal project — that's a poor fit. You want visitor messages to land somewhere your team already hangs out (Zulip / Discord), and you want the widget to set expectations that replies take hours, not seconds. That's what this is.

Architecture

widget (browser, Web Component)
   ↓  WebSocket
Cloudflare Worker → Durable Object (ChatServer)
   ├── ZulipAdapter  — event-queue polling, DM bot commands
   └── DiscordAdapter — Gateway WebSocket, slash commands, webhook send

Zulip topic  /  Discord thread  (one per conversation)

Conversations and config are persisted in Durable Object storage. The Discord Gateway connection is kept alive via a Durable Object Alarm chain (20-second interval).

Deploy your own

Prerequisites

1. Clone & install

git clone https://github.com/f3liz-casa/danro-talk
cd danro-talk
npm install

2. Configure Cloudflare

# Zulip credentials (read from ~/.zuliprc)
./scripts/set-secrets.sh

# Discord bot token (optional)
./scripts/set-discord-secret.sh

3. Deploy

npm run deploy

The worker is deployed to your-worker.workers.dev. To use a custom domain, add it to wrangler.toml:

[[routes]]
pattern = "your-domain.example.com"
custom_domain = true

4. Configure via bot DM (Zulip)

Send DMs to your Zulip bot (admin only):

Command What it does
set-stream <name> Set the stream for visitor messages
show Display current config and siteId
set-origin <domains> Comma-separated origin allowlist; empty to clear
rotate-id Issue a new siteId; old widgets stop working

On first start the bot auto-generates a siteId. Run show to retrieve it.

5. Configure via slash command (Discord)

Run /danro set-channel #your-channel in your server (requires Manage Server). The bot responds with the siteId and embed snippet.

Command What it does
/danro set-channel <channel> Set parent channel; issues siteId
/danro show Display current channel, siteId, allowed origins
/danro set-origin <domains> Comma-separated allowlist; empty to clear
/danro rotate-id Issue a new siteId; old widgets break, sessions close
/danro disable Remove configuration; active sessions close

6. Embed the widget

<script type="module" src="https://your-worker.workers.dev/widget.js"></script>

<!-- Zulip -->
<danro-talk site-id="zl_xxxxxxxxxxxx" lang="ja"></danro-talk>

<!-- Discord -->
<danro-talk site-id="dc_xxxxxxxxxxxx" lang="ko" target="discord"></danro-talk>

ws-url is optional — the widget auto-connects to the origin it was loaded from.

Local development

cp .env .dev.vars   # or: ln -s .env .dev.vars
npm run dev         # wrangler dev on :8787
npm run dev:widget  # rebuild widget on change
open widget/demo.html

Security model

The siteId is public (it lives in your site's HTML), so think of it as a routing token, not a secret:

  1. Origin allowlist (strongest) — once set, the server checks the browser-supplied Origin header on every WebSocket handshake. Browsers can't forge Origin, blocking casual embedding abuse.
  2. siteId — filters out misconfigured clients and accidental embeds.
  3. Rotate — incident response: rotate the siteId and old copies stop working immediately.
  4. Disable (Discord) — kill switch; configuration deleted, sessions closed.

Roadmap

License

MIT. See LICENSE.