Build Eloqra
KINDAreplaces $4.99/mosaves $59.88/yrback to the verdict
A small script tag that puts a chat widget on your site, a WebSocket server that relays messages to a Telegram bot, replies that go back to the visitor when you reply to the bot's message, and an optional Mini App console for when one conversation at a time stops being enough.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server, the socket and the webhook.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - API keyfree
Why It is the inbox. Without it there is nowhere for messages to go.
Get it Message BotFather in Telegram, create a bot, and take the token. If you want the Mini App in Phase 5, also register its URL and a menu button there. open ↗
Verify
A getMe call with the token returns your bot - have ready5 to 10 USD a month for a server, plus a domain
Why Telegram will not deliver webhooks to plain HTTP and will not load a Mini App over it. This is not a preference; it simply does not work.
Get it A small server with a domain and automatic certificates, or a tunnel that terminates TLS for local development.
Verify
setWebhook succeeds and a test update is delivered - have readyfree
Why Same-origin testing hides the CORS and origin problems Phase 1 exists to catch.
Get it Any static host, or a second local server on a different port, which is a different origin.
Verify
The widget loads and connects from that page - have readyfree
Why Phase 5's webview sizing, safe areas and long press copy cannot be judged in a desktop client.
Get it Any phone with Telegram. Test in both normal and fullscreen presentation.
Verify
The Mini App opens on the phone and fits correctly
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- sites(id, name, origin_allowlist, widget_key) - conversations(id, site_id, visitor_token, state, started_at, last_message_at, telegram_topic_id) - messages(id, conversation_id, seq, author, body, sent_at, telegram_message_id) · seq is a per-conversation monotonic integer - visitors(visitor_token, first_seen_at, last_seen_at, user_agent_class) · a token you issued, not an identity you inferred - The link between a Telegram message and a conversation is stored, because replying to a bot message is how an operator answers and that mapping is the whole mechanism.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
TELEGRAM_BOT_TOKENsecret | required | 123456:replace-with-token | From BotFather. Server only, never in client code or logs. |
TELEGRAM_CHAT_ID | required | 123456789 | Where visitor messages are forwarded. Your own chat, or a group. |
PUBLIC_URL | required | https://chat.example.com | Used for the webhook and the Mini App URL. Must be HTTPS. |
DB_PATH | required | ./data/chat.db | Conversations, messages and visitors. |
ORIGIN_ALLOWLIST | required | https://yoursite.com | Which origins may open a socket. Checked at handshake. |
VISITOR_TOKEN_SECRETsecret | required | generate-a-long-random-value | Signs visitor tokens so one cannot be forged or reused from another browser. |
MAX_NEW_CONVERSATIONS_PER_HOUR | required | 20 | Per site. Conversation creation is the expensive operation because each one becomes a notification. |
The build, in order
Widget, origin control and a socket
A script tag on your site opens a conversation, and an origin you did not authorise cannot.
Shadow root so the host page's styles cannot break it. No framework, no build step for the widget itself.
Files
public/widget.jsCheck the Origin header against the allowlist before accepting the connection.
Files
src/ws.jsSigned, server-issued, stored in local storage. Never generated in the browser.
Files
src/visitors.js
done when · tick each as it passeswatch out- The widget key ships in public HTML, so it identifies a site and grants nothing. Every authorisation decision belongs on the server, and the visitor token must be issued by the server rather than generated in the browser, or anyone can claim another visitor's conversation.
Messages into Telegram
A visitor's message arrives as a notification on your phone.
BotFather issues it. Server environment only, and never logged.
Files
.env.exampleSite, conversation and the message. Enough to answer without opening anything else.
Files
src/telegram/send.jsTelegram message identifier against conversation. This is what Phase 3 depends on.
Files
src/telegram/map.js
done when · tick each as it passeswatch out- The bot token is the whole of your bot's security and it appears in every API call. Keep it on the server only, never in the widget or any client, and rotate it immediately if it ever reaches a log you share.
Replying by replying
Replying to the bot's message in Telegram sends your answer to the visitor.
A real certificate. Telegram will not deliver to plain HTTP and the failure is silent.
terminalcurl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook
The update's reply target identifies the conversation. Without the mapping there is nowhere to send it.
Files
src/telegram/webhook.jsQueue and deliver on reconnection, using the sequence number so nothing arrives twice.
Files
src/deliver.js
done when · tick each as it passeswatch out- Telegram requires HTTPS for webhooks and will not deliver to plain HTTP at all, and the same applies to loading a Mini App later. Get a certificate before you start this phase rather than debugging silence afterwards.
Delivery that survives a dropped connection
Nothing is lost or duplicated when a visitor's connection drops.
Per conversation, with a unique constraint on conversation and sequence.
Files
src/messages.jsAn HTTP endpoint returning messages after a sequence, called before resuming the socket.
Files
src/backfill.jsBuffer live messages during backfill and deduplicate by sequence. This window is where duplicates come from.
Files
test/reconnect.test.js
done when · tick each as it passeswatch out- Sequencing by timestamp breaks under concurrent writes and clock differences, and two messages in the same millisecond reorder or collide. Use a monotonic integer allocated by the database with a unique constraint, so a duplicate insert fails loudly rather than quietly.
The Mini App console, if you need it
A real thread list inside Telegram, with the webview quirks handled.
Recompute the hash with your bot token and reject a mismatch. This is the authentication and it must be server side.
Files
src/miniapp/auth.jsUse the stable viewport height Telegram provides, not viewport units, and apply both safe area insets.
Files
src/miniapp/layout.cssSelection is disabled, so implement the affordance yourself and test it on a real phone.
Files
src/miniapp/copy.js
done when · tick each as it passeswatch out- The initialisation data Telegram passes must be verified by recomputing its hash on the server with your bot token. Trusting it unverified means anyone can claim to be any user.
- Dynamic viewport units overshoot inside Telegram's webview, producing an interface that extends under the interface chrome. Size from the stable viewport height Telegram reports, and apply both safe area insets in fullscreen or your header sits under the status bar.
- Text selection is disabled in the webview, so copying a message needs a long press handler you write yourself.
Running it, and the cost you acceptedproduct builder
It stays up, resists abuse, and you know what it costs against the free product.
Per visitor and per site, with a low ceiling on new conversations specifically.
Files
src/ratelimit.jsA window for old conversations, and a deletion covering messages and visitor rows together.
Files
src/retention.jsServer, domain and hours. Next to the free product's price, which is zero for one site.
Files
docs/cost.md
done when · tick each as it passeswatch out- A widget on a public page is an open endpoint, and conversation creation is the expensive operation because each one becomes a notification on your phone. Rate limit creation specifically and hard, or one script will fill your Telegram overnight.
That is the whole plan for Eloqra. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A real operator inbox unless you build the Mini App console.
- Handling several conversations simultaneously through bot replies.
- A free tier, since you are paying for a server.
- Multi-channel support beyond the site widget.
- a real operator inbox · the Mini App console with a combined thread list across sites, unread badges, typing indicators and deep links from a notification straight into the conversation · bot replies cover one chat at a time, not five at once
- the week you spend learning Telegram's webview rather than writing features · initData HMAC auth, sizing from viewportStableHeight because dvh overshoots, two safe-area insets to stay clear of the status bar in fullscreen, and text selection disabled so copying a message needs a hand-rolled long-press
- a free tier · you would be spending days plus a monthly VPS bill to replace something that costs nothing for one site
- everything that isn't the happy path · socket reconnects, duplicate sends, message ordering on flaky mobile networks, Telegram rate limits and retry queues, bot token rotation
- always-on hosting · your widget is dead whenever your box, your TLS cert, or your websocket is, and a chat that silently stops connecting reads to visitors as you ignoring them
- Forum topics in a Telegram group so each conversation is its own thread
- Canned replies triggered by a bot command
- Business hours with an automatic away message
Need the files? The project pack on the verdict page hands your agent the whole brief · more customer support.