Vibecode Eloqra
track this build5 phases, 15 steps, beginner friendly0%The first half is a one-sitting build and the second half isn't. A script tag that injects a chat panel, a websocket, and a bot that forwards messages so your native Telegram reply reaches the visitor · that works by dinner, and the prompt below builds it. The gap is the operator side. Replying inside a bot chat is fine for one conversation a week; the moment you have several going you want a real inbox, and in Telegram that means a Mini App, which is a second application with its own platform rules: auth is HMAC-validating initData against your bot token, the webview lies about its own height so you size from viewportStableHeight instead of dvh, fullscreen hides content under the status bar until you combine two different safe-area insets, and it disables text selection outright, so even copying a message means hand-rolling a long-press with a clipboard fallback. None of that is hard. All of it is discovered the slow way, on a phone, after the prompt has finished.
You are building a lean indie version of Eloqra. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Eloqra · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | Node 22 with a WebSocket server | the workload is a handful of idle connections and short bursts | | Inbox | A Telegram bot through the Bot API | push notifications, an app on every platform and threading, none of which you write | | Widget | A hand-written script under 15 KB | it loads on your visitors' pages and every kilobyte is theirs | | Storage | SQLite | conversations and messages for one site; anything larger is a different product | | Console | An optional Telegram Mini App | a real thread list when bot replies stop being enough, at the cost of a week in a webview | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the socket and the webhook. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Telegram bot token from BotFather** · free - 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. - Verify: A getMe call with the token returns your bot - [ ] **An HTTPS host with a real certificate** · 5 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 - [ ] **A page on a different origin** · free - 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 - [ ] **A real phone with Telegram installed** (optional) · free - 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 ## Quick start ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Eloqra, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Eloqra The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a site chat widget whose inbox is a Telegram bot, with a Mini App console when bot replies are not enough to replace Eloqra. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Be honest about the arithmetic before you start. The product you are replacing is free for one site. This build costs you days of work plus a monthly server bill, and the reason to do it anyway is control over the data and the ability to change how it works. If your reason is saving money on one site, the arithmetic does not support it. The second decision is how far to go. Bot replies handle one conversation at a time and take an afternoon. A Mini App console with a thread list, unread badges and deep links is a week of learning Telegram's webview quirks. Phases 1 to 3 give you the first; Phase 5 gives you the second, and it is optional on purpose. ### Stack (fixed, do not substitute) - Server: Node 22 with a WebSocket server · the workload is a handful of idle connections and short bursts - Inbox: A Telegram bot through the Bot API · push notifications, an app on every platform and threading, none of which you write - Widget: A hand-written script under 15 KB · it loads on your visitors' pages and every kilobyte is theirs - Storage: SQLite · conversations and messages for one site; anything larger is a different product - Console: An optional Telegram Mini App · a real thread list when bot replies stop being enough, at the cost of a week in a webview ### Data model (create this before Phase 1) - 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. ### Phase 1 · Widget, origin control and a socket Build: A small embeddable script creating a shadow root and a chat panel. A WebSocket server validating the Origin header against the site's allowlist at handshake. A visitor token issued by the server and stored locally so a refresh continues the same conversation. Trap: 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. Done when: The widget loads on your site from a different origin and opens a conversation. A connection from an unlisted origin is refused at handshake. Refreshing the page continues the same conversation. Presenting another visitor's token from a different browser is refused. Do not build yet: No Telegram at all yet. A working widget and a socket. ### Phase 2 · Messages into Telegram Build: Create a bot with BotFather and take its token. Forward each visitor message to your chat, including enough context to know which site and which conversation. Store the Telegram message identifier against the conversation, because that mapping is how replies find their way back. Trap: 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. Done when: A message typed in the widget arrives as a Telegram notification within a couple of seconds. The notification identifies which site and conversation it belongs to. The Telegram message identifier is stored against the conversation. The bot token appears nowhere in client code, verified by inspecting the served bundle. Do not build yet: No replies back to the visitor yet. ### Phase 3 · Replying by replying Build: Receive updates by webhook over HTTPS. When an update is a reply to a message you sent, look up the conversation and deliver the text over the visitor's socket. If the visitor is gone, hold the message and deliver it when they return. Trap: 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. Done when: Replying to a bot message in Telegram delivers the text to the visitor's widget within a couple of seconds. Replying while the visitor is away holds the message and delivers it when they return. A reply to a message with no stored mapping produces a clear error in the chat rather than silence. A message sent to the bot that is not a reply is answered with an explanation. Do not build yet: No Mini App yet; this is the afternoon version and it works. ### Phase 4 · Delivery that survives a dropped connection Build: Assign a per-conversation sequence number to every message. The widget stores the highest it has seen and requests everything after it on reconnection, over HTTP, before resuming live delivery. Trap: 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. Done when: Killing the socket, sending three replies from Telegram, then reconnecting delivers exactly those three in order. A message arriving during the backfill window is not delivered twice. The same widget message sent twice with one idempotency key stores one row. Do not build yet: The Mini App console is next and is optional. ### Phase 5 · The Mini App console, if you need it Build: Register a Mini App with BotFather. Verify the initialisation data by checking its hash server side. Size the interface from the viewport height Telegram reports rather than from viewport units. Respect the safe area insets. Provide your own copy affordance, because text selection is disabled in the webview. Trap: 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. Trap: 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. Trap: Text selection is disabled in the webview, so copying a message needs a long press handler you write yourself. Done when: The Mini App opens from the bot menu and lists conversations across sites with unread counts. Initialisation data is verified server side and a tampered payload is rejected. The interface fits without scrolling under Telegram's chrome, in both normal and fullscreen. A long press copies a message. Do not build yet: Operations come last. ### Phase 6 · Running it, and the cost you accepted Build: Rate limit conversation creation per visitor and per site. Retention with a documented deletion path. TLS, monitoring and backups. Then record the actual monthly cost and the hours spent, against the free alternative. Trap: 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. Done when: A burst of automated conversation creation is throttled and existing conversations stay responsive. Retention removes old conversations and a deletion request removes one visitor's messages entirely. A restore onto a clean server reproduces conversations. The monthly cost and hours spent are written down next to the free alternative. Do not build yet: Nothing after this. ### Out of scope (and why) - A real operator inbox with a combined thread list, typing indicators and deep links from a notification into a conversation, unless you build Phase 5, which is a week. - Answering five conversations at once. Bot replies handle one at a time, and that limitation is the reason the Mini App exists. - A free tier. You are paying for a server to replace something that costs nothing for one site, and the reason has to be control rather than price. - Multi-channel support across email, social and other messengers. ### README must contain - That this costs a server bill and days of work to replace a product that is free for one site, and why you did it anyway - That the widget key is public and every authorisation decision is server side - That Telegram requires HTTPS for webhooks and Mini Apps, with no plain HTTP path - How sequence numbers and backfill guarantee exactly-once delivery - That Mini App initialisation data is verified server side, and the webview sizing and selection quirks ===== AGENTS.md ===== # Agent instructions · Eloqra indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with a WebSocket server, A Telegram bot through the Bot API, A hand-written script under 15 KB, SQLite, An optional Telegram Mini App. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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 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. ===== BUILD_PLAN.md ===== # Build plan · Eloqra 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Widget, origin control and a socket A script tag on your site opens a conversation, and an origin you did not authorise cannot. ### Steps 1. Write the widget Shadow root so the host page's styles cannot break it. No framework, no build step for the widget itself. Files: `public/widget.js` 2. Validate origin at handshake Check the Origin header against the allowlist before accepting the connection. Files: `src/ws.js` 3. Issue visitor tokens server side Signed, server-issued, stored in local storage. Never generated in the browser. Files: `src/visitors.js` ### Done when - [ ] The widget works from a different origin - [ ] An unlisted origin is refused at handshake - [ ] A refresh continues the same conversation - [ ] A stolen token from another browser is refused ### Watch 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. ## Phase 2 · Messages into Telegram A visitor's message arrives as a notification on your phone. ### Steps 1. Create the bot and store the token BotFather issues it. Server environment only, and never logged. Files: `.env.example` 2. Forward with context Site, conversation and the message. Enough to answer without opening anything else. Files: `src/telegram/send.js` 3. Store the message mapping Telegram message identifier against conversation. This is what Phase 3 depends on. Files: `src/telegram/map.js` ### Done when - [ ] A widget message arrives as a Telegram notification quickly - [ ] The notification identifies site and conversation - [ ] The Telegram message identifier is stored - [ ] The bot token is absent from all client code ### Watch 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. ## Phase 3 · Replying by replying Replying to the bot's message in Telegram sends your answer to the visitor. ### Steps 1. Set the webhook over HTTPS A real certificate. Telegram will not deliver to plain HTTP and the failure is silent. ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` 2. Route replies by the stored mapping The update's reply target identifies the conversation. Without the mapping there is nowhere to send it. Files: `src/telegram/webhook.js` 3. Hold messages for absent visitors Queue and deliver on reconnection, using the sequence number so nothing arrives twice. Files: `src/deliver.js` ### Done when - [ ] A Telegram reply reaches the visitor quickly - [ ] A reply while the visitor is away is delivered on their return - [ ] A reply with no mapping produces a clear error - [ ] A non-reply message to the bot is answered with an explanation ### Watch 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. ## Phase 4 · Delivery that survives a dropped connection Nothing is lost or duplicated when a visitor's connection drops. ### Steps 1. Allocate sequence numbers in the database Per conversation, with a unique constraint on conversation and sequence. Files: `src/messages.js` 2. Backfill on reconnect An HTTP endpoint returning messages after a sequence, called before resuming the socket. Files: `src/backfill.js` 3. Handle the overlap Buffer live messages during backfill and deduplicate by sequence. This window is where duplicates come from. Files: `test/reconnect.test.js` ### Done when - [ ] Three messages sent while offline arrive exactly once - [ ] Messages during backfill are not delivered twice - [ ] A duplicate sequence insert fails at the database - [ ] One idempotency key stores one row ### Watch 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. ## Phase 5 · The Mini App console, if you need it A real thread list inside Telegram, with the webview quirks handled. ### Steps 1. Register and verify initialisation data 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.js` 2. Size from the reported viewport Use the stable viewport height Telegram provides, not viewport units, and apply both safe area insets. Files: `src/miniapp/layout.css` 3. Add a long press copy Selection is disabled, so implement the affordance yourself and test it on a real phone. Files: `src/miniapp/copy.js` ### Done when - [ ] The Mini App lists conversations with unread counts - [ ] Tampered initialisation data is rejected - [ ] The interface fits correctly in normal and fullscreen - [ ] Long press copies a message on a real phone ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. From BotFather. Server only, never in client code or logs. TELEGRAM_BOT_TOKEN=123456:replace-with-token # Required. Where visitor messages are forwarded. Your own chat, or a group. TELEGRAM_CHAT_ID=123456789 # Required. Used for the webhook and the Mini App URL. Must be HTTPS. PUBLIC_URL=https://chat.example.com # Required. Conversations, messages and visitors. DB_PATH=./data/chat.db # Required. Which origins may open a socket. Checked at handshake. ORIGIN_ALLOWLIST=https://yoursite.com # Required · secret. Signs visitor tokens so one cannot be forged or reused from another browser. VISITOR_TOKEN_SECRET=generate-a-long-random-value # Required. Per site. Conversation creation is the expensive operation because each one becomes a notification. MAX_NEW_CONVERSATIONS_PER_HOUR=20
You are building a lean indie version of Eloqra. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Eloqra · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | Node 22 with a WebSocket server | the workload is a handful of idle connections and short bursts | | Inbox | A Telegram bot through the Bot API | push notifications, an app on every platform and threading, none of which you write | | Widget | A hand-written script under 15 KB | it loads on your visitors' pages and every kilobyte is theirs | | Storage | SQLite | conversations and messages for one site; anything larger is a different product | | Console | An optional Telegram Mini App | a real thread list when bot replies stop being enough, at the cost of a week in a webview | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the socket and the webhook. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Telegram bot token from BotFather** · free - 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. - Verify: A getMe call with the token returns your bot - [ ] **An HTTPS host with a real certificate** · 5 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 - [ ] **A page on a different origin** · free - 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 - [ ] **A real phone with Telegram installed** (optional) · free - 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 ## Quick start ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Eloqra, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Eloqra The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a site chat widget whose inbox is a Telegram bot, with a Mini App console when bot replies are not enough to replace Eloqra. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Be honest about the arithmetic before you start. The product you are replacing is free for one site. This build costs you days of work plus a monthly server bill, and the reason to do it anyway is control over the data and the ability to change how it works. If your reason is saving money on one site, the arithmetic does not support it. The second decision is how far to go. Bot replies handle one conversation at a time and take an afternoon. A Mini App console with a thread list, unread badges and deep links is a week of learning Telegram's webview quirks. Phases 1 to 3 give you the first; Phase 5 gives you the second, and it is optional on purpose. ### Stack (fixed, do not substitute) - Server: Node 22 with a WebSocket server · the workload is a handful of idle connections and short bursts - Inbox: A Telegram bot through the Bot API · push notifications, an app on every platform and threading, none of which you write - Widget: A hand-written script under 15 KB · it loads on your visitors' pages and every kilobyte is theirs - Storage: SQLite · conversations and messages for one site; anything larger is a different product - Console: An optional Telegram Mini App · a real thread list when bot replies stop being enough, at the cost of a week in a webview ### Data model (create this before Phase 1) - 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. ### Phase 1 · Widget, origin control and a socket Build: A small embeddable script creating a shadow root and a chat panel. A WebSocket server validating the Origin header against the site's allowlist at handshake. A visitor token issued by the server and stored locally so a refresh continues the same conversation. Trap: 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. Done when: The widget loads on your site from a different origin and opens a conversation. A connection from an unlisted origin is refused at handshake. Refreshing the page continues the same conversation. Presenting another visitor's token from a different browser is refused. Do not build yet: No Telegram at all yet. A working widget and a socket. ### Phase 2 · Messages into Telegram Build: Create a bot with BotFather and take its token. Forward each visitor message to your chat, including enough context to know which site and which conversation. Store the Telegram message identifier against the conversation, because that mapping is how replies find their way back. Trap: 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. Done when: A message typed in the widget arrives as a Telegram notification within a couple of seconds. The notification identifies which site and conversation it belongs to. The Telegram message identifier is stored against the conversation. The bot token appears nowhere in client code, verified by inspecting the served bundle. Do not build yet: No replies back to the visitor yet. ### Phase 3 · Replying by replying Build: Receive updates by webhook over HTTPS. When an update is a reply to a message you sent, look up the conversation and deliver the text over the visitor's socket. If the visitor is gone, hold the message and deliver it when they return. Trap: 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. Done when: Replying to a bot message in Telegram delivers the text to the visitor's widget within a couple of seconds. Replying while the visitor is away holds the message and delivers it when they return. A reply to a message with no stored mapping produces a clear error in the chat rather than silence. A message sent to the bot that is not a reply is answered with an explanation. Do not build yet: No Mini App yet; this is the afternoon version and it works. ### Phase 4 · Delivery that survives a dropped connection Build: Assign a per-conversation sequence number to every message. The widget stores the highest it has seen and requests everything after it on reconnection, over HTTP, before resuming live delivery. Trap: 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. Done when: Killing the socket, sending three replies from Telegram, then reconnecting delivers exactly those three in order. A message arriving during the backfill window is not delivered twice. The same widget message sent twice with one idempotency key stores one row. Do not build yet: The Mini App console is next and is optional. ### Phase 5 · The Mini App console, if you need it Build: Register a Mini App with BotFather. Verify the initialisation data by checking its hash server side. Size the interface from the viewport height Telegram reports rather than from viewport units. Respect the safe area insets. Provide your own copy affordance, because text selection is disabled in the webview. Trap: 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. Trap: 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. Trap: Text selection is disabled in the webview, so copying a message needs a long press handler you write yourself. Done when: The Mini App opens from the bot menu and lists conversations across sites with unread counts. Initialisation data is verified server side and a tampered payload is rejected. The interface fits without scrolling under Telegram's chrome, in both normal and fullscreen. A long press copies a message. Do not build yet: Operations come last. ### Phase 6 · Running it, and the cost you accepted Build: Rate limit conversation creation per visitor and per site. Retention with a documented deletion path. TLS, monitoring and backups. Then record the actual monthly cost and the hours spent, against the free alternative. Trap: 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. Done when: A burst of automated conversation creation is throttled and existing conversations stay responsive. Retention removes old conversations and a deletion request removes one visitor's messages entirely. A restore onto a clean server reproduces conversations. The monthly cost and hours spent are written down next to the free alternative. Do not build yet: Nothing after this. ### Out of scope (and why) - A real operator inbox with a combined thread list, typing indicators and deep links from a notification into a conversation, unless you build Phase 5, which is a week. - Answering five conversations at once. Bot replies handle one at a time, and that limitation is the reason the Mini App exists. - A free tier. You are paying for a server to replace something that costs nothing for one site, and the reason has to be control rather than price. - Multi-channel support across email, social and other messengers. ### README must contain - That this costs a server bill and days of work to replace a product that is free for one site, and why you did it anyway - That the widget key is public and every authorisation decision is server side - That Telegram requires HTTPS for webhooks and Mini Apps, with no plain HTTP path - How sequence numbers and backfill guarantee exactly-once delivery - That Mini App initialisation data is verified server side, and the webview sizing and selection quirks ===== AGENTS.md ===== # Agent instructions · Eloqra indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with a WebSocket server, A Telegram bot through the Bot API, A hand-written script under 15 KB, SQLite, An optional Telegram Mini App. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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 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. ===== BUILD_PLAN.md ===== # Build plan · Eloqra 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Widget, origin control and a socket A script tag on your site opens a conversation, and an origin you did not authorise cannot. ### Steps 1. Write the widget Shadow root so the host page's styles cannot break it. No framework, no build step for the widget itself. Files: `public/widget.js` 2. Validate origin at handshake Check the Origin header against the allowlist before accepting the connection. Files: `src/ws.js` 3. Issue visitor tokens server side Signed, server-issued, stored in local storage. Never generated in the browser. Files: `src/visitors.js` ### Done when - [ ] The widget works from a different origin - [ ] An unlisted origin is refused at handshake - [ ] A refresh continues the same conversation - [ ] A stolen token from another browser is refused ### Watch 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. ## Phase 2 · Messages into Telegram A visitor's message arrives as a notification on your phone. ### Steps 1. Create the bot and store the token BotFather issues it. Server environment only, and never logged. Files: `.env.example` 2. Forward with context Site, conversation and the message. Enough to answer without opening anything else. Files: `src/telegram/send.js` 3. Store the message mapping Telegram message identifier against conversation. This is what Phase 3 depends on. Files: `src/telegram/map.js` ### Done when - [ ] A widget message arrives as a Telegram notification quickly - [ ] The notification identifies site and conversation - [ ] The Telegram message identifier is stored - [ ] The bot token is absent from all client code ### Watch 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. ## Phase 3 · Replying by replying Replying to the bot's message in Telegram sends your answer to the visitor. ### Steps 1. Set the webhook over HTTPS A real certificate. Telegram will not deliver to plain HTTP and the failure is silent. ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` 2. Route replies by the stored mapping The update's reply target identifies the conversation. Without the mapping there is nowhere to send it. Files: `src/telegram/webhook.js` 3. Hold messages for absent visitors Queue and deliver on reconnection, using the sequence number so nothing arrives twice. Files: `src/deliver.js` ### Done when - [ ] A Telegram reply reaches the visitor quickly - [ ] A reply while the visitor is away is delivered on their return - [ ] A reply with no mapping produces a clear error - [ ] A non-reply message to the bot is answered with an explanation ### Watch 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. ## Phase 4 · Delivery that survives a dropped connection Nothing is lost or duplicated when a visitor's connection drops. ### Steps 1. Allocate sequence numbers in the database Per conversation, with a unique constraint on conversation and sequence. Files: `src/messages.js` 2. Backfill on reconnect An HTTP endpoint returning messages after a sequence, called before resuming the socket. Files: `src/backfill.js` 3. Handle the overlap Buffer live messages during backfill and deduplicate by sequence. This window is where duplicates come from. Files: `test/reconnect.test.js` ### Done when - [ ] Three messages sent while offline arrive exactly once - [ ] Messages during backfill are not delivered twice - [ ] A duplicate sequence insert fails at the database - [ ] One idempotency key stores one row ### Watch 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. ## Phase 5 · The Mini App console, if you need it A real thread list inside Telegram, with the webview quirks handled. ### Steps 1. Register and verify initialisation data 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.js` 2. Size from the reported viewport Use the stable viewport height Telegram provides, not viewport units, and apply both safe area insets. Files: `src/miniapp/layout.css` 3. Add a long press copy Selection is disabled, so implement the affordance yourself and test it on a real phone. Files: `src/miniapp/copy.js` ### Done when - [ ] The Mini App lists conversations with unread counts - [ ] Tampered initialisation data is rejected - [ ] The interface fits correctly in normal and fullscreen - [ ] Long press copies a message on a real phone ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. From BotFather. Server only, never in client code or logs. TELEGRAM_BOT_TOKEN=123456:replace-with-token # Required. Where visitor messages are forwarded. Your own chat, or a group. TELEGRAM_CHAT_ID=123456789 # Required. Used for the webhook and the Mini App URL. Must be HTTPS. PUBLIC_URL=https://chat.example.com # Required. Conversations, messages and visitors. DB_PATH=./data/chat.db # Required. Which origins may open a socket. Checked at handshake. ORIGIN_ALLOWLIST=https://yoursite.com # Required · secret. Signs visitor tokens so one cannot be forged or reused from another browser. VISITOR_TOKEN_SECRET=generate-a-long-random-value # Required. Per site. Conversation creation is the expensive operation because each one becomes a notification. MAX_NEW_CONVERSATIONS_PER_HOUR=20
You are building a production product version of Eloqra. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Eloqra · product brief ## Problem The first half is a one-sitting build and the second half isn't. A script tag that injects a chat panel, a websocket, and a bot that forwards messages so your native Telegram reply reaches the visitor · that works by dinner, and the prompt below builds it. The gap is the operator side. Replying inside a bot chat is fine for one conversation a week; the moment you have several going you want a real inbox, and in Telegram that means a Mini App, which is a second application with its own platform rules: auth is HMAC-validating initData against your bot token, the webview lies about its own height so you size from viewportStableHeight instead of dvh, fullscreen hides content under the status bar until you combine two different safe-area insets, and it disables text selection outright, so even copying a message means hand-rolling a long-press with a clipboard fallback. None of that is hard. All of it is discovered the slow way, on a phone, after the prompt has finished. ## Product outcome A chat widget whose inbox is an application you already have on your phone, with delivery you can prove is exactly-once, an optional console for when replies alone stop working, and an honest record of what it cost to replace something free. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a Telegram bot token from @BotFather - an HTTPS host for the websocket · Telegram will not load a Mini App over plain http - BotFather registration of the Mini App URL and menu button, if you want a real inbox rather than bot replies ## Explicit non-goals for v1 - 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 ## Success criteria - The reconnect test proves exactly-once delivery - The bot token is confirmed absent from all client code - An unlisted origin is confirmed refused at handshake - A visitor deletion is confirmed to remove all their messages - The cost comparison against the free alternative is written down ===== BRIEF.md ===== # Build brief · Eloqra The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a site chat widget whose inbox is a Telegram bot, with a Mini App console when bot replies are not enough to replace Eloqra. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Be honest about the arithmetic before you start. The product you are replacing is free for one site. This build costs you days of work plus a monthly server bill, and the reason to do it anyway is control over the data and the ability to change how it works. If your reason is saving money on one site, the arithmetic does not support it. The second decision is how far to go. Bot replies handle one conversation at a time and take an afternoon. A Mini App console with a thread list, unread badges and deep links is a week of learning Telegram's webview quirks. Phases 1 to 3 give you the first; Phase 5 gives you the second, and it is optional on purpose. ### Stack (fixed, do not substitute) - Server: Node 22 with a WebSocket server · the workload is a handful of idle connections and short bursts - Inbox: A Telegram bot through the Bot API · push notifications, an app on every platform and threading, none of which you write - Widget: A hand-written script under 15 KB · it loads on your visitors' pages and every kilobyte is theirs - Storage: SQLite · conversations and messages for one site; anything larger is a different product - Console: An optional Telegram Mini App · a real thread list when bot replies stop being enough, at the cost of a week in a webview ### Data model (create this before Phase 1) - 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. ### Phase 1 · Widget, origin control and a socket Build: A small embeddable script creating a shadow root and a chat panel. A WebSocket server validating the Origin header against the site's allowlist at handshake. A visitor token issued by the server and stored locally so a refresh continues the same conversation. Trap: 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. Done when: The widget loads on your site from a different origin and opens a conversation. A connection from an unlisted origin is refused at handshake. Refreshing the page continues the same conversation. Presenting another visitor's token from a different browser is refused. Do not build yet: No Telegram at all yet. A working widget and a socket. ### Phase 2 · Messages into Telegram Build: Create a bot with BotFather and take its token. Forward each visitor message to your chat, including enough context to know which site and which conversation. Store the Telegram message identifier against the conversation, because that mapping is how replies find their way back. Trap: 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. Done when: A message typed in the widget arrives as a Telegram notification within a couple of seconds. The notification identifies which site and conversation it belongs to. The Telegram message identifier is stored against the conversation. The bot token appears nowhere in client code, verified by inspecting the served bundle. Do not build yet: No replies back to the visitor yet. ### Phase 3 · Replying by replying Build: Receive updates by webhook over HTTPS. When an update is a reply to a message you sent, look up the conversation and deliver the text over the visitor's socket. If the visitor is gone, hold the message and deliver it when they return. Trap: 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. Done when: Replying to a bot message in Telegram delivers the text to the visitor's widget within a couple of seconds. Replying while the visitor is away holds the message and delivers it when they return. A reply to a message with no stored mapping produces a clear error in the chat rather than silence. A message sent to the bot that is not a reply is answered with an explanation. Do not build yet: No Mini App yet; this is the afternoon version and it works. ### Phase 4 · Delivery that survives a dropped connection Build: Assign a per-conversation sequence number to every message. The widget stores the highest it has seen and requests everything after it on reconnection, over HTTP, before resuming live delivery. Trap: 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. Done when: Killing the socket, sending three replies from Telegram, then reconnecting delivers exactly those three in order. A message arriving during the backfill window is not delivered twice. The same widget message sent twice with one idempotency key stores one row. Do not build yet: The Mini App console is next and is optional. ### Phase 5 · The Mini App console, if you need it Build: Register a Mini App with BotFather. Verify the initialisation data by checking its hash server side. Size the interface from the viewport height Telegram reports rather than from viewport units. Respect the safe area insets. Provide your own copy affordance, because text selection is disabled in the webview. Trap: 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. Trap: 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. Trap: Text selection is disabled in the webview, so copying a message needs a long press handler you write yourself. Done when: The Mini App opens from the bot menu and lists conversations across sites with unread counts. Initialisation data is verified server side and a tampered payload is rejected. The interface fits without scrolling under Telegram's chrome, in both normal and fullscreen. A long press copies a message. Do not build yet: Operations come last. ### Phase 6 · Running it, and the cost you accepted Build: Rate limit conversation creation per visitor and per site. Retention with a documented deletion path. TLS, monitoring and backups. Then record the actual monthly cost and the hours spent, against the free alternative. Trap: 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. Done when: A burst of automated conversation creation is throttled and existing conversations stay responsive. Retention removes old conversations and a deletion request removes one visitor's messages entirely. A restore onto a clean server reproduces conversations. The monthly cost and hours spent are written down next to the free alternative. Do not build yet: Nothing after this. ### Out of scope (and why) - A real operator inbox with a combined thread list, typing indicators and deep links from a notification into a conversation, unless you build Phase 5, which is a week. - Answering five conversations at once. Bot replies handle one at a time, and that limitation is the reason the Mini App exists. - A free tier. You are paying for a server to replace something that costs nothing for one site, and the reason has to be control rather than price. - Multi-channel support across email, social and other messengers. ### README must contain - That this costs a server bill and days of work to replace a product that is free for one site, and why you did it anyway - That the widget key is public and every authorisation decision is server side - That Telegram requires HTTPS for webhooks and Mini Apps, with no plain HTTP path - How sequence numbers and backfill guarantee exactly-once delivery - That Mini App initialisation data is verified server side, and the webview sizing and selection quirks ===== ARCHITECTURE.md ===== # Architecture · Eloqra ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | Node 22 with a WebSocket server | the workload is a handful of idle connections and short bursts | | Inbox | A Telegram bot through the Bot API | push notifications, an app on every platform and threading, none of which you write | | Widget | A hand-written script under 15 KB | it loads on your visitors' pages and every kilobyte is theirs | | Storage | SQLite | conversations and messages for one site; anything larger is a different product | | Console | An optional Telegram Mini App | a real thread list when bot replies stop being enough, at the cost of a week in a webview | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Widget | The visitor-facing panel and the socket. | Hand-written and small on purpose, because it loads on your visitors' pages. | | Relay | Moving messages between socket and bot. | Another messenger is another adapter. The conversation mapping is the part that stays. | | Messages | Sequence allocation and backfill. | Not swappable in practice. The sequence contract is what makes reconnection correct. | | Mini App | The optional console. | Entirely optional. Phases 1 to 4 are a complete product without it, which is why it comes last. | | Limits | Rate limiting and retention. | Any implementation, provided conversation creation is limited hardest. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TELEGRAM_BOT_TOKEN` · required, secret · From BotFather. Server only, never in client code or logs. - `TELEGRAM_CHAT_ID` · required · Where visitor messages are forwarded. Your own chat, or a group. - `PUBLIC_URL` · required · Used for the webhook and the Mini App URL. Must be HTTPS. - `DB_PATH` · required · Conversations, messages and visitors. - `ORIGIN_ALLOWLIST` · required · Which origins may open a socket. Checked at handshake. - `VISITOR_TOKEN_SECRET` · required, secret · Signs visitor tokens so one cannot be forged or reused from another browser. - `MAX_NEW_CONVERSATIONS_PER_HOUR` · required · Per site. Conversation creation is the expensive operation because each one becomes a notification. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Eloqra product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with a WebSocket server, A Telegram bot through the Bot API, A hand-written script under 15 KB, SQLite, An optional Telegram Mini App. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Eloqra Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Widget, origin control and a socket A script tag on your site opens a conversation, and an origin you did not authorise cannot. ### Steps 1. Write the widget Shadow root so the host page's styles cannot break it. No framework, no build step for the widget itself. Files: `public/widget.js` 2. Validate origin at handshake Check the Origin header against the allowlist before accepting the connection. Files: `src/ws.js` 3. Issue visitor tokens server side Signed, server-issued, stored in local storage. Never generated in the browser. Files: `src/visitors.js` ### Done when - [ ] The widget works from a different origin - [ ] An unlisted origin is refused at handshake - [ ] A refresh continues the same conversation - [ ] A stolen token from another browser is refused ### Watch 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. ## M2 · Messages into Telegram A visitor's message arrives as a notification on your phone. ### Steps 1. Create the bot and store the token BotFather issues it. Server environment only, and never logged. Files: `.env.example` 2. Forward with context Site, conversation and the message. Enough to answer without opening anything else. Files: `src/telegram/send.js` 3. Store the message mapping Telegram message identifier against conversation. This is what Phase 3 depends on. Files: `src/telegram/map.js` ### Done when - [ ] A widget message arrives as a Telegram notification quickly - [ ] The notification identifies site and conversation - [ ] The Telegram message identifier is stored - [ ] The bot token is absent from all client code ### Watch 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. ## M3 · Replying by replying Replying to the bot's message in Telegram sends your answer to the visitor. ### Steps 1. Set the webhook over HTTPS A real certificate. Telegram will not deliver to plain HTTP and the failure is silent. ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` 2. Route replies by the stored mapping The update's reply target identifies the conversation. Without the mapping there is nowhere to send it. Files: `src/telegram/webhook.js` 3. Hold messages for absent visitors Queue and deliver on reconnection, using the sequence number so nothing arrives twice. Files: `src/deliver.js` ### Done when - [ ] A Telegram reply reaches the visitor quickly - [ ] A reply while the visitor is away is delivered on their return - [ ] A reply with no mapping produces a clear error - [ ] A non-reply message to the bot is answered with an explanation ### Watch 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. ## M4 · Delivery that survives a dropped connection Nothing is lost or duplicated when a visitor's connection drops. ### Steps 1. Allocate sequence numbers in the database Per conversation, with a unique constraint on conversation and sequence. Files: `src/messages.js` 2. Backfill on reconnect An HTTP endpoint returning messages after a sequence, called before resuming the socket. Files: `src/backfill.js` 3. Handle the overlap Buffer live messages during backfill and deduplicate by sequence. This window is where duplicates come from. Files: `test/reconnect.test.js` ### Done when - [ ] Three messages sent while offline arrive exactly once - [ ] Messages during backfill are not delivered twice - [ ] A duplicate sequence insert fails at the database - [ ] One idempotency key stores one row ### Watch 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. ## M5 · The Mini App console, if you need it A real thread list inside Telegram, with the webview quirks handled. ### Steps 1. Register and verify initialisation data 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.js` 2. Size from the reported viewport Use the stable viewport height Telegram provides, not viewport units, and apply both safe area insets. Files: `src/miniapp/layout.css` 3. Add a long press copy Selection is disabled, so implement the affordance yourself and test it on a real phone. Files: `src/miniapp/copy.js` ### Done when - [ ] The Mini App lists conversations with unread counts - [ ] Tampered initialisation data is rejected - [ ] The interface fits correctly in normal and fullscreen - [ ] Long press copies a message on a real phone ### Watch 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. ## M6 · Running it, and the cost you accepted (production only) It stays up, resists abuse, and you know what it costs against the free product. ### Steps 1. Rate limit creation hardest Per visitor and per site, with a low ceiling on new conversations specifically. Files: `src/ratelimit.js` 2. Retention and deletion A window for old conversations, and a deletion covering messages and visitor rows together. Files: `src/retention.js` 3. Record the real cost Server, domain and hours. Next to the free product's price, which is zero for one site. Files: `docs/cost.md` ### Done when - [ ] A creation burst is throttled without affecting existing conversations - [ ] A deletion request removes a visitor's messages entirely - [ ] A restore reproduces conversations on a clean server - [ ] The cost comparison is written down honestly ### Watch 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. ===== OPERATIONS.md ===== # Operations · Eloqra ## Backup The database holds conversations and messages, which are other people's words about their problems. Back it up, encrypt the backup, and apply the retention window rather than keeping everything forever. ## Restore Restore the database, reset the webhook, and send a test message through the widget. The webhook is the step people forget, and its absence is silent. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Webhook delivery failures, socket count and conversation creation rate. A spike in creation is the signal for abuse rather than popularity, and it costs you notifications rather than money. ## Incident checklist If notifications stop, check the webhook first: Telegram removes it after sustained failures and does not tell you. If the bot token leaks, revoke it through BotFather immediately, which invalidates it everywhere. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The reconnect test proves exactly-once delivery - [ ] The bot token is confirmed absent from all client code - [ ] An unlisted origin is confirmed refused at handshake - [ ] A visitor deletion is confirmed to remove all their messages - [ ] The cost comparison against the free alternative is written down ## Launch constraint Do not market omitted Eloqra capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. From BotFather. Server only, never in client code or logs. TELEGRAM_BOT_TOKEN=123456:replace-with-token # Required. Where visitor messages are forwarded. Your own chat, or a group. TELEGRAM_CHAT_ID=123456789 # Required. Used for the webhook and the Mini App URL. Must be HTTPS. PUBLIC_URL=https://chat.example.com # Required. Conversations, messages and visitors. DB_PATH=./data/chat.db # Required. Which origins may open a socket. Checked at handshake. ORIGIN_ALLOWLIST=https://yoursite.com # Required · secret. Signs visitor tokens so one cannot be forged or reused from another browser. VISITOR_TOKEN_SECRET=generate-a-long-random-value # Required. Per site. Conversation creation is the expensive operation because each one becomes a notification. MAX_NEW_CONVERSATIONS_PER_HOUR=20
# Eloqra · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | Node 22 with a WebSocket server | the workload is a handful of idle connections and short bursts | | Inbox | A Telegram bot through the Bot API | push notifications, an app on every platform and threading, none of which you write | | Widget | A hand-written script under 15 KB | it loads on your visitors' pages and every kilobyte is theirs | | Storage | SQLite | conversations and messages for one site; anything larger is a different product | | Console | An optional Telegram Mini App | a real thread list when bot replies stop being enough, at the cost of a week in a webview | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the socket and the webhook. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Telegram bot token from BotFather** · free - 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. - Verify: A getMe call with the token returns your bot - [ ] **An HTTPS host with a real certificate** · 5 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 - [ ] **A page on a different origin** · free - 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 - [ ] **A real phone with Telegram installed** (optional) · free - 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 ## Quick start ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Eloqra, and the README should say so rather than pretend.
# Build brief · Eloqra The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a site chat widget whose inbox is a Telegram bot, with a Mini App console when bot replies are not enough to replace Eloqra. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Be honest about the arithmetic before you start. The product you are replacing is free for one site. This build costs you days of work plus a monthly server bill, and the reason to do it anyway is control over the data and the ability to change how it works. If your reason is saving money on one site, the arithmetic does not support it. The second decision is how far to go. Bot replies handle one conversation at a time and take an afternoon. A Mini App console with a thread list, unread badges and deep links is a week of learning Telegram's webview quirks. Phases 1 to 3 give you the first; Phase 5 gives you the second, and it is optional on purpose. ### Stack (fixed, do not substitute) - Server: Node 22 with a WebSocket server · the workload is a handful of idle connections and short bursts - Inbox: A Telegram bot through the Bot API · push notifications, an app on every platform and threading, none of which you write - Widget: A hand-written script under 15 KB · it loads on your visitors' pages and every kilobyte is theirs - Storage: SQLite · conversations and messages for one site; anything larger is a different product - Console: An optional Telegram Mini App · a real thread list when bot replies stop being enough, at the cost of a week in a webview ### Data model (create this before Phase 1) - 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. ### Phase 1 · Widget, origin control and a socket Build: A small embeddable script creating a shadow root and a chat panel. A WebSocket server validating the Origin header against the site's allowlist at handshake. A visitor token issued by the server and stored locally so a refresh continues the same conversation. Trap: 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. Done when: The widget loads on your site from a different origin and opens a conversation. A connection from an unlisted origin is refused at handshake. Refreshing the page continues the same conversation. Presenting another visitor's token from a different browser is refused. Do not build yet: No Telegram at all yet. A working widget and a socket. ### Phase 2 · Messages into Telegram Build: Create a bot with BotFather and take its token. Forward each visitor message to your chat, including enough context to know which site and which conversation. Store the Telegram message identifier against the conversation, because that mapping is how replies find their way back. Trap: 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. Done when: A message typed in the widget arrives as a Telegram notification within a couple of seconds. The notification identifies which site and conversation it belongs to. The Telegram message identifier is stored against the conversation. The bot token appears nowhere in client code, verified by inspecting the served bundle. Do not build yet: No replies back to the visitor yet. ### Phase 3 · Replying by replying Build: Receive updates by webhook over HTTPS. When an update is a reply to a message you sent, look up the conversation and deliver the text over the visitor's socket. If the visitor is gone, hold the message and deliver it when they return. Trap: 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. Done when: Replying to a bot message in Telegram delivers the text to the visitor's widget within a couple of seconds. Replying while the visitor is away holds the message and delivers it when they return. A reply to a message with no stored mapping produces a clear error in the chat rather than silence. A message sent to the bot that is not a reply is answered with an explanation. Do not build yet: No Mini App yet; this is the afternoon version and it works. ### Phase 4 · Delivery that survives a dropped connection Build: Assign a per-conversation sequence number to every message. The widget stores the highest it has seen and requests everything after it on reconnection, over HTTP, before resuming live delivery. Trap: 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. Done when: Killing the socket, sending three replies from Telegram, then reconnecting delivers exactly those three in order. A message arriving during the backfill window is not delivered twice. The same widget message sent twice with one idempotency key stores one row. Do not build yet: The Mini App console is next and is optional. ### Phase 5 · The Mini App console, if you need it Build: Register a Mini App with BotFather. Verify the initialisation data by checking its hash server side. Size the interface from the viewport height Telegram reports rather than from viewport units. Respect the safe area insets. Provide your own copy affordance, because text selection is disabled in the webview. Trap: 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. Trap: 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. Trap: Text selection is disabled in the webview, so copying a message needs a long press handler you write yourself. Done when: The Mini App opens from the bot menu and lists conversations across sites with unread counts. Initialisation data is verified server side and a tampered payload is rejected. The interface fits without scrolling under Telegram's chrome, in both normal and fullscreen. A long press copies a message. Do not build yet: Operations come last. ### Phase 6 · Running it, and the cost you accepted Build: Rate limit conversation creation per visitor and per site. Retention with a documented deletion path. TLS, monitoring and backups. Then record the actual monthly cost and the hours spent, against the free alternative. Trap: 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. Done when: A burst of automated conversation creation is throttled and existing conversations stay responsive. Retention removes old conversations and a deletion request removes one visitor's messages entirely. A restore onto a clean server reproduces conversations. The monthly cost and hours spent are written down next to the free alternative. Do not build yet: Nothing after this. ### Out of scope (and why) - A real operator inbox with a combined thread list, typing indicators and deep links from a notification into a conversation, unless you build Phase 5, which is a week. - Answering five conversations at once. Bot replies handle one at a time, and that limitation is the reason the Mini App exists. - A free tier. You are paying for a server to replace something that costs nothing for one site, and the reason has to be control rather than price. - Multi-channel support across email, social and other messengers. ### README must contain - That this costs a server bill and days of work to replace a product that is free for one site, and why you did it anyway - That the widget key is public and every authorisation decision is server side - That Telegram requires HTTPS for webhooks and Mini Apps, with no plain HTTP path - How sequence numbers and backfill guarantee exactly-once delivery - That Mini App initialisation data is verified server side, and the webview sizing and selection quirks
# Agent instructions · Eloqra indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with a WebSocket server, A Telegram bot through the Bot API, A hand-written script under 15 KB, SQLite, An optional Telegram Mini App. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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 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.
# Build plan · Eloqra 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Widget, origin control and a socket A script tag on your site opens a conversation, and an origin you did not authorise cannot. ### Steps 1. Write the widget Shadow root so the host page's styles cannot break it. No framework, no build step for the widget itself. Files: `public/widget.js` 2. Validate origin at handshake Check the Origin header against the allowlist before accepting the connection. Files: `src/ws.js` 3. Issue visitor tokens server side Signed, server-issued, stored in local storage. Never generated in the browser. Files: `src/visitors.js` ### Done when - [ ] The widget works from a different origin - [ ] An unlisted origin is refused at handshake - [ ] A refresh continues the same conversation - [ ] A stolen token from another browser is refused ### Watch 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. ## Phase 2 · Messages into Telegram A visitor's message arrives as a notification on your phone. ### Steps 1. Create the bot and store the token BotFather issues it. Server environment only, and never logged. Files: `.env.example` 2. Forward with context Site, conversation and the message. Enough to answer without opening anything else. Files: `src/telegram/send.js` 3. Store the message mapping Telegram message identifier against conversation. This is what Phase 3 depends on. Files: `src/telegram/map.js` ### Done when - [ ] A widget message arrives as a Telegram notification quickly - [ ] The notification identifies site and conversation - [ ] The Telegram message identifier is stored - [ ] The bot token is absent from all client code ### Watch 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. ## Phase 3 · Replying by replying Replying to the bot's message in Telegram sends your answer to the visitor. ### Steps 1. Set the webhook over HTTPS A real certificate. Telegram will not deliver to plain HTTP and the failure is silent. ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` 2. Route replies by the stored mapping The update's reply target identifies the conversation. Without the mapping there is nowhere to send it. Files: `src/telegram/webhook.js` 3. Hold messages for absent visitors Queue and deliver on reconnection, using the sequence number so nothing arrives twice. Files: `src/deliver.js` ### Done when - [ ] A Telegram reply reaches the visitor quickly - [ ] A reply while the visitor is away is delivered on their return - [ ] A reply with no mapping produces a clear error - [ ] A non-reply message to the bot is answered with an explanation ### Watch 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. ## Phase 4 · Delivery that survives a dropped connection Nothing is lost or duplicated when a visitor's connection drops. ### Steps 1. Allocate sequence numbers in the database Per conversation, with a unique constraint on conversation and sequence. Files: `src/messages.js` 2. Backfill on reconnect An HTTP endpoint returning messages after a sequence, called before resuming the socket. Files: `src/backfill.js` 3. Handle the overlap Buffer live messages during backfill and deduplicate by sequence. This window is where duplicates come from. Files: `test/reconnect.test.js` ### Done when - [ ] Three messages sent while offline arrive exactly once - [ ] Messages during backfill are not delivered twice - [ ] A duplicate sequence insert fails at the database - [ ] One idempotency key stores one row ### Watch 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. ## Phase 5 · The Mini App console, if you need it A real thread list inside Telegram, with the webview quirks handled. ### Steps 1. Register and verify initialisation data 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.js` 2. Size from the reported viewport Use the stable viewport height Telegram provides, not viewport units, and apply both safe area insets. Files: `src/miniapp/layout.css` 3. Add a long press copy Selection is disabled, so implement the affordance yourself and test it on a real phone. Files: `src/miniapp/copy.js` ### Done when - [ ] The Mini App lists conversations with unread counts - [ ] Tampered initialisation data is rejected - [ ] The interface fits correctly in normal and fullscreen - [ ] Long press copies a message on a real phone ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. From BotFather. Server only, never in client code or logs. TELEGRAM_BOT_TOKEN=123456:replace-with-token # Required. Where visitor messages are forwarded. Your own chat, or a group. TELEGRAM_CHAT_ID=123456789 # Required. Used for the webhook and the Mini App URL. Must be HTTPS. PUBLIC_URL=https://chat.example.com # Required. Conversations, messages and visitors. DB_PATH=./data/chat.db # Required. Which origins may open a socket. Checked at handshake. ORIGIN_ALLOWLIST=https://yoursite.com # Required · secret. Signs visitor tokens so one cannot be forged or reused from another browser. VISITOR_TOKEN_SECRET=generate-a-long-random-value # Required. Per site. Conversation creation is the expensive operation because each one becomes a notification. MAX_NEW_CONVERSATIONS_PER_HOUR=20
# Eloqra · product brief ## Problem The first half is a one-sitting build and the second half isn't. A script tag that injects a chat panel, a websocket, and a bot that forwards messages so your native Telegram reply reaches the visitor · that works by dinner, and the prompt below builds it. The gap is the operator side. Replying inside a bot chat is fine for one conversation a week; the moment you have several going you want a real inbox, and in Telegram that means a Mini App, which is a second application with its own platform rules: auth is HMAC-validating initData against your bot token, the webview lies about its own height so you size from viewportStableHeight instead of dvh, fullscreen hides content under the status bar until you combine two different safe-area insets, and it disables text selection outright, so even copying a message means hand-rolling a long-press with a clipboard fallback. None of that is hard. All of it is discovered the slow way, on a phone, after the prompt has finished. ## Product outcome A chat widget whose inbox is an application you already have on your phone, with delivery you can prove is exactly-once, an optional console for when replies alone stop working, and an honest record of what it cost to replace something free. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a Telegram bot token from @BotFather - an HTTPS host for the websocket · Telegram will not load a Mini App over plain http - BotFather registration of the Mini App URL and menu button, if you want a real inbox rather than bot replies ## Explicit non-goals for v1 - 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 ## Success criteria - The reconnect test proves exactly-once delivery - The bot token is confirmed absent from all client code - An unlisted origin is confirmed refused at handshake - A visitor deletion is confirmed to remove all their messages - The cost comparison against the free alternative is written down
# Build brief · Eloqra The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a site chat widget whose inbox is a Telegram bot, with a Mini App console when bot replies are not enough to replace Eloqra. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Be honest about the arithmetic before you start. The product you are replacing is free for one site. This build costs you days of work plus a monthly server bill, and the reason to do it anyway is control over the data and the ability to change how it works. If your reason is saving money on one site, the arithmetic does not support it. The second decision is how far to go. Bot replies handle one conversation at a time and take an afternoon. A Mini App console with a thread list, unread badges and deep links is a week of learning Telegram's webview quirks. Phases 1 to 3 give you the first; Phase 5 gives you the second, and it is optional on purpose. ### Stack (fixed, do not substitute) - Server: Node 22 with a WebSocket server · the workload is a handful of idle connections and short bursts - Inbox: A Telegram bot through the Bot API · push notifications, an app on every platform and threading, none of which you write - Widget: A hand-written script under 15 KB · it loads on your visitors' pages and every kilobyte is theirs - Storage: SQLite · conversations and messages for one site; anything larger is a different product - Console: An optional Telegram Mini App · a real thread list when bot replies stop being enough, at the cost of a week in a webview ### Data model (create this before Phase 1) - 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. ### Phase 1 · Widget, origin control and a socket Build: A small embeddable script creating a shadow root and a chat panel. A WebSocket server validating the Origin header against the site's allowlist at handshake. A visitor token issued by the server and stored locally so a refresh continues the same conversation. Trap: 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. Done when: The widget loads on your site from a different origin and opens a conversation. A connection from an unlisted origin is refused at handshake. Refreshing the page continues the same conversation. Presenting another visitor's token from a different browser is refused. Do not build yet: No Telegram at all yet. A working widget and a socket. ### Phase 2 · Messages into Telegram Build: Create a bot with BotFather and take its token. Forward each visitor message to your chat, including enough context to know which site and which conversation. Store the Telegram message identifier against the conversation, because that mapping is how replies find their way back. Trap: 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. Done when: A message typed in the widget arrives as a Telegram notification within a couple of seconds. The notification identifies which site and conversation it belongs to. The Telegram message identifier is stored against the conversation. The bot token appears nowhere in client code, verified by inspecting the served bundle. Do not build yet: No replies back to the visitor yet. ### Phase 3 · Replying by replying Build: Receive updates by webhook over HTTPS. When an update is a reply to a message you sent, look up the conversation and deliver the text over the visitor's socket. If the visitor is gone, hold the message and deliver it when they return. Trap: 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. Done when: Replying to a bot message in Telegram delivers the text to the visitor's widget within a couple of seconds. Replying while the visitor is away holds the message and delivers it when they return. A reply to a message with no stored mapping produces a clear error in the chat rather than silence. A message sent to the bot that is not a reply is answered with an explanation. Do not build yet: No Mini App yet; this is the afternoon version and it works. ### Phase 4 · Delivery that survives a dropped connection Build: Assign a per-conversation sequence number to every message. The widget stores the highest it has seen and requests everything after it on reconnection, over HTTP, before resuming live delivery. Trap: 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. Done when: Killing the socket, sending three replies from Telegram, then reconnecting delivers exactly those three in order. A message arriving during the backfill window is not delivered twice. The same widget message sent twice with one idempotency key stores one row. Do not build yet: The Mini App console is next and is optional. ### Phase 5 · The Mini App console, if you need it Build: Register a Mini App with BotFather. Verify the initialisation data by checking its hash server side. Size the interface from the viewport height Telegram reports rather than from viewport units. Respect the safe area insets. Provide your own copy affordance, because text selection is disabled in the webview. Trap: 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. Trap: 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. Trap: Text selection is disabled in the webview, so copying a message needs a long press handler you write yourself. Done when: The Mini App opens from the bot menu and lists conversations across sites with unread counts. Initialisation data is verified server side and a tampered payload is rejected. The interface fits without scrolling under Telegram's chrome, in both normal and fullscreen. A long press copies a message. Do not build yet: Operations come last. ### Phase 6 · Running it, and the cost you accepted Build: Rate limit conversation creation per visitor and per site. Retention with a documented deletion path. TLS, monitoring and backups. Then record the actual monthly cost and the hours spent, against the free alternative. Trap: 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. Done when: A burst of automated conversation creation is throttled and existing conversations stay responsive. Retention removes old conversations and a deletion request removes one visitor's messages entirely. A restore onto a clean server reproduces conversations. The monthly cost and hours spent are written down next to the free alternative. Do not build yet: Nothing after this. ### Out of scope (and why) - A real operator inbox with a combined thread list, typing indicators and deep links from a notification into a conversation, unless you build Phase 5, which is a week. - Answering five conversations at once. Bot replies handle one at a time, and that limitation is the reason the Mini App exists. - A free tier. You are paying for a server to replace something that costs nothing for one site, and the reason has to be control rather than price. - Multi-channel support across email, social and other messengers. ### README must contain - That this costs a server bill and days of work to replace a product that is free for one site, and why you did it anyway - That the widget key is public and every authorisation decision is server side - That Telegram requires HTTPS for webhooks and Mini Apps, with no plain HTTP path - How sequence numbers and backfill guarantee exactly-once delivery - That Mini App initialisation data is verified server side, and the webview sizing and selection quirks
# Architecture · Eloqra ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | Node 22 with a WebSocket server | the workload is a handful of idle connections and short bursts | | Inbox | A Telegram bot through the Bot API | push notifications, an app on every platform and threading, none of which you write | | Widget | A hand-written script under 15 KB | it loads on your visitors' pages and every kilobyte is theirs | | Storage | SQLite | conversations and messages for one site; anything larger is a different product | | Console | An optional Telegram Mini App | a real thread list when bot replies stop being enough, at the cost of a week in a webview | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Widget | The visitor-facing panel and the socket. | Hand-written and small on purpose, because it loads on your visitors' pages. | | Relay | Moving messages between socket and bot. | Another messenger is another adapter. The conversation mapping is the part that stays. | | Messages | Sequence allocation and backfill. | Not swappable in practice. The sequence contract is what makes reconnection correct. | | Mini App | The optional console. | Entirely optional. Phases 1 to 4 are a complete product without it, which is why it comes last. | | Limits | Rate limiting and retention. | Any implementation, provided conversation creation is limited hardest. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TELEGRAM_BOT_TOKEN` · required, secret · From BotFather. Server only, never in client code or logs. - `TELEGRAM_CHAT_ID` · required · Where visitor messages are forwarded. Your own chat, or a group. - `PUBLIC_URL` · required · Used for the webhook and the Mini App URL. Must be HTTPS. - `DB_PATH` · required · Conversations, messages and visitors. - `ORIGIN_ALLOWLIST` · required · Which origins may open a socket. Checked at handshake. - `VISITOR_TOKEN_SECRET` · required, secret · Signs visitor tokens so one cannot be forged or reused from another browser. - `MAX_NEW_CONVERSATIONS_PER_HOUR` · required · Per site. Conversation creation is the expensive operation because each one becomes a notification. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Eloqra product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with a WebSocket server, A Telegram bot through the Bot API, A hand-written script under 15 KB, SQLite, An optional Telegram Mini App. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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 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. - 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.
# Delivery milestones · Eloqra Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Widget, origin control and a socket A script tag on your site opens a conversation, and an origin you did not authorise cannot. ### Steps 1. Write the widget Shadow root so the host page's styles cannot break it. No framework, no build step for the widget itself. Files: `public/widget.js` 2. Validate origin at handshake Check the Origin header against the allowlist before accepting the connection. Files: `src/ws.js` 3. Issue visitor tokens server side Signed, server-issued, stored in local storage. Never generated in the browser. Files: `src/visitors.js` ### Done when - [ ] The widget works from a different origin - [ ] An unlisted origin is refused at handshake - [ ] A refresh continues the same conversation - [ ] A stolen token from another browser is refused ### Watch 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. ## M2 · Messages into Telegram A visitor's message arrives as a notification on your phone. ### Steps 1. Create the bot and store the token BotFather issues it. Server environment only, and never logged. Files: `.env.example` 2. Forward with context Site, conversation and the message. Enough to answer without opening anything else. Files: `src/telegram/send.js` 3. Store the message mapping Telegram message identifier against conversation. This is what Phase 3 depends on. Files: `src/telegram/map.js` ### Done when - [ ] A widget message arrives as a Telegram notification quickly - [ ] The notification identifies site and conversation - [ ] The Telegram message identifier is stored - [ ] The bot token is absent from all client code ### Watch 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. ## M3 · Replying by replying Replying to the bot's message in Telegram sends your answer to the visitor. ### Steps 1. Set the webhook over HTTPS A real certificate. Telegram will not deliver to plain HTTP and the failure is silent. ```sh curl -F "url=https://your.host/tg/webhook" https://api.telegram.org/bot$TOKEN/setWebhook ``` 2. Route replies by the stored mapping The update's reply target identifies the conversation. Without the mapping there is nowhere to send it. Files: `src/telegram/webhook.js` 3. Hold messages for absent visitors Queue and deliver on reconnection, using the sequence number so nothing arrives twice. Files: `src/deliver.js` ### Done when - [ ] A Telegram reply reaches the visitor quickly - [ ] A reply while the visitor is away is delivered on their return - [ ] A reply with no mapping produces a clear error - [ ] A non-reply message to the bot is answered with an explanation ### Watch 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. ## M4 · Delivery that survives a dropped connection Nothing is lost or duplicated when a visitor's connection drops. ### Steps 1. Allocate sequence numbers in the database Per conversation, with a unique constraint on conversation and sequence. Files: `src/messages.js` 2. Backfill on reconnect An HTTP endpoint returning messages after a sequence, called before resuming the socket. Files: `src/backfill.js` 3. Handle the overlap Buffer live messages during backfill and deduplicate by sequence. This window is where duplicates come from. Files: `test/reconnect.test.js` ### Done when - [ ] Three messages sent while offline arrive exactly once - [ ] Messages during backfill are not delivered twice - [ ] A duplicate sequence insert fails at the database - [ ] One idempotency key stores one row ### Watch 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. ## M5 · The Mini App console, if you need it A real thread list inside Telegram, with the webview quirks handled. ### Steps 1. Register and verify initialisation data 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.js` 2. Size from the reported viewport Use the stable viewport height Telegram provides, not viewport units, and apply both safe area insets. Files: `src/miniapp/layout.css` 3. Add a long press copy Selection is disabled, so implement the affordance yourself and test it on a real phone. Files: `src/miniapp/copy.js` ### Done when - [ ] The Mini App lists conversations with unread counts - [ ] Tampered initialisation data is rejected - [ ] The interface fits correctly in normal and fullscreen - [ ] Long press copies a message on a real phone ### Watch 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. ## M6 · Running it, and the cost you accepted (production only) It stays up, resists abuse, and you know what it costs against the free product. ### Steps 1. Rate limit creation hardest Per visitor and per site, with a low ceiling on new conversations specifically. Files: `src/ratelimit.js` 2. Retention and deletion A window for old conversations, and a deletion covering messages and visitor rows together. Files: `src/retention.js` 3. Record the real cost Server, domain and hours. Next to the free product's price, which is zero for one site. Files: `docs/cost.md` ### Done when - [ ] A creation burst is throttled without affecting existing conversations - [ ] A deletion request removes a visitor's messages entirely - [ ] A restore reproduces conversations on a clean server - [ ] The cost comparison is written down honestly ### Watch 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.
# Operations · Eloqra ## Backup The database holds conversations and messages, which are other people's words about their problems. Back it up, encrypt the backup, and apply the retention window rather than keeping everything forever. ## Restore Restore the database, reset the webhook, and send a test message through the widget. The webhook is the step people forget, and its absence is silent. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Webhook delivery failures, socket count and conversation creation rate. A spike in creation is the signal for abuse rather than popularity, and it costs you notifications rather than money. ## Incident checklist If notifications stop, check the webhook first: Telegram removes it after sustained failures and does not tell you. If the bot token leaks, revoke it through BotFather immediately, which invalidates it everywhere. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The reconnect test proves exactly-once delivery - [ ] The bot token is confirmed absent from all client code - [ ] An unlisted origin is confirmed refused at handshake - [ ] A visitor deletion is confirmed to remove all their messages - [ ] The cost comparison against the free alternative is written down ## Launch constraint Do not market omitted Eloqra capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. From BotFather. Server only, never in client code or logs. TELEGRAM_BOT_TOKEN=123456:replace-with-token # Required. Where visitor messages are forwarded. Your own chat, or a group. TELEGRAM_CHAT_ID=123456789 # Required. Used for the webhook and the Mini App URL. Must be HTTPS. PUBLIC_URL=https://chat.example.com # Required. Conversations, messages and visitors. DB_PATH=./data/chat.db # Required. Which origins may open a socket. Checked at handshake. ORIGIN_ALLOWLIST=https://yoursite.com # Required · secret. Signs visitor tokens so one cannot be forged or reused from another browser. VISITOR_TOKEN_SECRET=generate-a-long-random-value # Required. Per site. Conversation creation is the expensive operation because each one becomes a notification. MAX_NEW_CONVERSATIONS_PER_HOUR=20
$ choose a build depth, inspect the files, then open the complete pack in your agent
Mostly they don't · for one website there is nothing to pay. Free is forever, with unlimited conversations, the full Telegram inbox and mini-app, and every widget option; 4.99 buys an extra project slot for a second site and nothing more, and the 'powered by' badge appears on paid tiers too. So the usual death-list logic inverts here. You aren't weighing a weekend against a subscription, you're weighing a weekend plus a VPS, TLS renewals, reconnect logic, and Telegram retry handling against zero. Live chat is also a promise that someone is actually there, which makes it a bad thing to hang off a box you maintain between other work. The one honest reason to build your own: you want visitor conversations on your own hardware instead of someone else's. That is the entire argument, and it isn't a financial one.
xa 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
xthe 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
xa free tier · you would be spending days plus a monthly VPS bill to replace something that costs nothing for one site
xeverything 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
xalways-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
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Eloqra pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | 1 website project; unlimited operators/seats and no per-message metering |
| pro (each additional project) | $4.99/workspace | $4.17/workspace | 1 additional project per subscription; no per-seat or per-message charge |
free tier1 project forever; no seat cap and no message cap stated
billingmonthly $4.99 or annual $49.99 per additional project; first project stays free
hidden costsPricing scales per additional website project, not per seat or message; there are no stated overages.
verified 2026-08-12 · source ↗
Vibecode Eloqra
Kinda. The core of Eloqra is buildable in a weekend with the prompt on this page, but there are real gaps: 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. Read the honest list above before committing.
How much does Eloqra cost?
Eloqra costs about $4.99/month (Pro, checked 2026-07-31), which is $59.88 per year.
What do I lose by replacing Eloqra?
Honestly: 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. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Eloqra?
Yes: Live Helper Chat (Website chats can land in Telegram and replies go back; exact trick, decidedly non-trivial setup.) The prompt is for when you want it exactly your way.