@qemuguest/site-chat-server
v0.1.0
Published
Backend for website live chat: Web-standard handlers, storage adapters (D1, SQLite, memory) and relays to SimpleX (one chat per customer), Telegram and email.
Maintainers
Readme
@qemuguest/site-chat-server
Backend for website live chat. Web-standard handlers (Request → Response), pluggable storage, and relays
to SimpleX (one chat per customer, via @qemuguest/site-chat-simplex-bridge),
Telegram (two-way) and email. Pairs with @qemuguest/site-chat-widget.
npm i @qemuguest/site-chat-serverimport { createSiteChat, sqlStore, d1Driver, simplexRelay, telegram, emailAlerts, resendEmail } from "@qemuguest/site-chat-server";
const chat = createSiteChat({
store: sqlStore(d1Driver(env.DB)),
basePath: "/api/site-chat", // where you mount it (default)
channels: [
simplexRelay({ secret: env.SITE_CHAT_SECRET }),
telegram({ botToken: env.TELEGRAM_BOT_TOKEN, webhookSecret: env.TELEGRAM_WEBHOOK_SECRET }),
emailAlerts({ to: "[email protected]", send: resendEmail({ apiKey: env.RESEND_API_KEY, from: "Site <[email protected]>" }) }),
],
adminUrl: (id) => `https://example.com/admin/messages?c=${id}`, // linked from alerts
});
// Any framework: hand it the Request.
export default { fetch: (req: Request) => chat.fetch(req) }; // Cloudflare Worker
export const GET = (req: Request) => chat.fetch(req), POST = GET; // Next.js app/api/site-chat/[...path]/route.ts
app.all("/api/site-chat/*", (c) => chat.fetch(c.req.raw)); // Honochat.handle(req) resolves to null for paths that aren't its own, so you can mount it alongside other routes.
Endpoints (under basePath)
| Method & path | Who | What |
| --- | --- | --- |
| POST /messages | widget | start / continue a chat, send { text } or { details: { name, email, phone } } |
| GET /messages?id&token&after | widget | poll for replies |
| POST /contact | widget | contact form { name, email, phone?, business?, message } |
| POST /telegram/webhook | Telegram | bot updates (checked against webhookSecret) |
| GET /relay/outbox, POST /relay/ack, POST /relay/reply | SimpleX bridge | Authorization: Bearer <secret> |
Built in: per-IP limit on new conversations, per-conversation message limit, honeypot field, visitor tokens (only the visitor can read their thread), IPs stored only as salted hashes.
Storage
memoryStore()for tests and dev.sqlStore(driver, { prefix: "sc_" })withd1Driver(env.DB)(Cloudflare D1) orsqliteDriver(db)(better-sqlite3,node:sqlite). Create tables withawait store.migrate()or runschemaSql()as a migration.- Or implement the
ChatStoreinterface for anything else.
Admin
await chat.admin.list(); // conversations with last message + unread state
await chat.admin.get(id); // { conversation, messages }
await chat.admin.reply(id, "On it!"); // appears live in the visitor's chat, mirrored to Telegram / SimpleX
await chat.admin.markRead(id);
await chat.admin.unreadCount();Telegram
- Create a bot with @BotFather; pick any long random
webhookSecret. const tg = telegram({ botToken, webhookSecret }), pass it inchannels, then once:const { link } = await tg.connect("https://example.com/api/site-chat/telegram/webhook").- Open
linkand press Start. That chat is now the only one that gets messages and can answer. Reply to a relayed message to answer the visitor live.
SimpleX
Add simplexRelay({ secret }) and run the bridge next to your SimpleX app. See
@qemuguest/site-chat-simplex-bridge.
Custom channels
const slack: Channel = {
name: "slack",
async notify(e, ctx) { if (e.type === "visitor") await postToSlack(e.conversation, e.body); },
routes: { async "POST /slack/events"(req, ctx) { /* … */ await ctx.agentReply(conversationId, text, "slack"); return new Response("ok"); } },
};MIT
