<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: iMonitor Software</title>
    <description>The latest articles on DEV Community by iMonitor Software (@imonitorsoft).</description>
    <link>https://dev.to/imonitorsoft</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203838%2Fed60546b-9873-4657-a63a-e777afb1db3e.png</url>
      <title>DEV Community: iMonitor Software</title>
      <link>https://dev.to/imonitorsoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imonitorsoft"/>
    <language>en</language>
    <item>
      <title>Employee monitoring reading list: nine languages, seventeen guides each</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 16:59:51 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/employee-monitoring-reading-list-nine-languages-seventeen-guides-each-3m02</link>
      <guid>https://dev.to/imonitorsoft/employee-monitoring-reading-list-nine-languages-seventeen-guides-each-3m02</guid>
      <description>&lt;p&gt;We write about employee monitoring in nine languages — not translated, but written for each market. This is the index, so you can jump straight to your language instead of scrolling through a hundred posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nine language guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/es/index.html" rel="noopener noreferrer"&gt;Español&lt;/a&gt; — 17 guías&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/de/index.html" rel="noopener noreferrer"&gt;Deutsch&lt;/a&gt; — 17 Leitfäden&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/fr/index.html" rel="noopener noreferrer"&gt;Français&lt;/a&gt; — 17 guides&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/it/index.html" rel="noopener noreferrer"&gt;Italiano&lt;/a&gt; — 17 guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/pt/index.html" rel="noopener noreferrer"&gt;Português&lt;/a&gt; — 17 guias&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/jp/index.html" rel="noopener noreferrer"&gt;日本語&lt;/a&gt; — 17 ガイド&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/kr/index.html" rel="noopener noreferrer"&gt;한국어&lt;/a&gt; — 17 가이드&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/tr/index.html" rel="noopener noreferrer"&gt;Türkçe&lt;/a&gt; — 17 rehber&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/big5/index.html" rel="noopener noreferrer"&gt;繁體中文&lt;/a&gt; — 17 篇指南&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What each language covers
&lt;/h2&gt;

&lt;p&gt;The same seventeen topics, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hybrid work: what monitoring should answer&lt;/li&gt;
&lt;li&gt;BYOD: what may be recorded on a personal device&lt;/li&gt;
&lt;li&gt;Retention: how long to keep, and how to justify it&lt;/li&gt;
&lt;li&gt;When an employee objects: the first hour&lt;/li&gt;
&lt;li&gt;Work time and payroll accuracy&lt;/li&gt;
&lt;li&gt;A 30-day rollout that does not break trust&lt;/li&gt;
&lt;li&gt;What happens to monitoring data when someone leaves&lt;/li&gt;
&lt;li&gt;Server and storage sizing, with the actual formula&lt;/li&gt;
&lt;li&gt;On-premise vs cloud: five differences that matter&lt;/li&gt;
&lt;li&gt;Counting keystrokes instead of recording content&lt;/li&gt;
&lt;li&gt;Five cases where you should not buy this software&lt;/li&gt;
&lt;li&gt;Reporting to management: three numbers, not screenshots&lt;/li&gt;
&lt;li&gt;The three realistic ways monitoring data leaks&lt;/li&gt;
&lt;li&gt;The five questions your team will ask&lt;/li&gt;
&lt;li&gt;Companies without HR: who owns this&lt;/li&gt;
&lt;li&gt;Three signals someone is leaving (and what data cannot see)&lt;/li&gt;
&lt;li&gt;Four costs that never appear in the quote&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The two pages that are not articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/es/pricing.html" rel="noopener noreferrer"&gt;Pricing and licensing&lt;/a&gt; — how per-seat perpetual licensing works, plus the FAQ we get most&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://imonitor-eam-guides.surge.sh/es/compliance.html" rel="noopener noreferrer"&gt;What we record and what we do not&lt;/a&gt; — the six-section version, including the fields we refuse to collect&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a separate host
&lt;/h2&gt;

&lt;p&gt;The main site is on its own infrastructure; this is an independent mirror so the guides stay readable even when infrastructure has a bad day. Everything is static HTML, no tracking scripts, no cookie banner to dismiss.&lt;/p&gt;

&lt;p&gt;If you are evaluating monitoring software, start with items 10 and 11 — they tell you fastest whether the product thinks the way you do.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;General information, not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The five questions your team will ask about monitoring (and answers that hold)</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:32:56 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/the-five-questions-your-team-will-ask-about-monitoring-and-answers-that-hold-5gck</link>
      <guid>https://dev.to/imonitorsoft/the-five-questions-your-team-will-ask-about-monitoring-and-answers-that-hold-5gck</guid>
      <description>&lt;p&gt;Announce monitoring and the same five questions appear, in the same order, almost every time. Having the answer written in advance — in the exact wording you will use in the room — removes about half of the conflict before it starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Are you watching me?"
&lt;/h2&gt;

&lt;p&gt;The answer that holds: &lt;em&gt;"We record working time and which applications are used. We do not record what you type — we count keystrokes, never content."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Do not answer only "this is not surveillance". If you skip what &lt;em&gt;is&lt;/em&gt; recorded, people fill the gap with the worst case. Name the fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "Does this affect my review?"
&lt;/h2&gt;

&lt;p&gt;The honest answer depends on the policy — and that policy has to exist before installation, not after. If the data will appear in a performance conversation, say so. If it will not, write that down too: &lt;em&gt;"this data is not used for sanctions; it is used to find process problems."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The failure mode is leaving it ambiguous and letting people assume the worst.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Can I see my own data?"
&lt;/h2&gt;

&lt;p&gt;Yes — and there should be a one-page procedure for asking, sitting next to the one for correcting an error. Refusing is the step that turns a management tool into a legal problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "What about outside working hours?"
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Activity outside your working hours is not recorded."&lt;/em&gt; And that has to be true in the configuration, not just in the presentation. Somebody will check.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. "What if I use my own laptop or phone?"
&lt;/h2&gt;

&lt;p&gt;The phone is out of scope. With BYOD, only work applications are covered — and you say which ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worst possible answer
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"If you have nothing to hide, you have nothing to fear."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It sounds like a defence and is heard as a threat. It also concedes the frame: that the purpose is suspicion. Change the frame instead — the system exists to answer a specific question: shifts that do not add up, overtime nobody approved, files leaving the company. Name the problem, not the suspicion.&lt;/p&gt;

&lt;h2&gt;
  
  
  One sentence that buys more trust than any policy
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"If in three months we have not made a single decision with this data, we turn it off."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Almost nobody says it, which is why it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the announcement
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write the five answers down, in the language people actually speak at work&lt;/li&gt;
&lt;li&gt;Decide, in writing, what the data will never be used for&lt;/li&gt;
&lt;li&gt;Name who can see raw data versus the monthly report&lt;/li&gt;
&lt;li&gt;Have the correction procedure ready &lt;em&gt;before&lt;/em&gt; someone needs it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The announcement is a one-hour event. These five answers are what people repeat to each other for the next two years.&lt;/p&gt;




&lt;p&gt;The nine-language versions of this piece are at &lt;a href="https://imonitor-eam-guides.surge.sh/" rel="noopener noreferrer"&gt;imonitor-eam-guides.surge.sh&lt;/a&gt; — same structure, written for the local market rather than translated. &lt;a href="https://imonitor-eam-guides.surge.sh/es/pricing.html" rel="noopener noreferrer"&gt;iMonitor EAM&lt;/a&gt; is licensed per seat, on your own server.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;General information, not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>management</category>
      <category>monitoring</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Reporting monitoring results to management: three numbers, not screenshots</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:12:31 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/reporting-monitoring-results-to-management-three-numbers-not-screenshots-58ee</link>
      <guid>https://dev.to/imonitorsoft/reporting-monitoring-results-to-management-three-numbers-not-screenshots-58ee</guid>
      <description>&lt;p&gt;The first time someone brings monitoring results into a leadership meeting, they almost always bring screenshots. It is the most common mistake, and it fails twice: a screenshot cannot be compared to anything, and in a room it reads as surveillance rather than management.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Coverage.&lt;/strong&gt; What share of the workforce is actually recorded, with the agent running? If it is 70%, the rest of the report is decoration. Coverage is the confidence interval on every other number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deviation rate.&lt;/strong&gt; What share of person-days falls outside the expected pattern? Around 3% is healthy. 30% does not mean people are not working — it means the baseline is wrong, and the report is measuring your assumptions rather than their work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Time to conversation.&lt;/strong&gt; How many days pass between an anomaly appearing and someone talking to that person about it? If the answer is "never", the system produces suspicion and nothing else. This is the number that predicts whether the rollout survives year two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why screenshots are the wrong instrument
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No context.&lt;/strong&gt; A capture does not show that the person covered two roles for three weeks, or that the ticket queue tripled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No comparability.&lt;/strong&gt; One frame cannot be trended, so it cannot support a decision, only an impression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No scale.&lt;/strong&gt; Nobody reviews 200 captures. The one selected for the meeting is always the most striking, not the most representative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It invites the wrong conversation.&lt;/strong&gt; The meeting drifts to "is this legal / is this spying" instead of "do we need a second shift".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Compare months, not people
&lt;/h2&gt;

&lt;p&gt;"Weekly active hours in support dropped from 6.1 to 5.4 after the shift change" leads to a staffing decision.&lt;/p&gt;

&lt;p&gt;"Marta averaged 4.2 and Paul 7.8" leads nowhere, because you cannot act on it without first deciding whether the difference is skill, workload, or equipment — and that decision needs data you do not have.&lt;/p&gt;

&lt;p&gt;Team-level trends survive scrutiny. Individual comparisons invite disputes you will lose on procedure, not on facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A one-page format
&lt;/h2&gt;

&lt;p&gt;Three short paragraphs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What changed&lt;/strong&gt; — one sentence, dated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which number shows it&lt;/strong&gt; — the trend, not the single frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What was decided&lt;/strong&gt; — the action, the owner, the date to review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If there is no decision, the third paragraph does not exist, and the report should probably not be sent. Monitoring data that never changes a decision is a cost with a liability attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that ruins the data
&lt;/h2&gt;

&lt;p&gt;If the first report is used to sanction someone, the second year of data will be false. People learn quickly which signal is rewarded, and they will produce that signal.&lt;/p&gt;

&lt;p&gt;Say it in writing, on day one: what the data is for, and what it will not be used for. Then hold that line the first time it is inconvenient — that moment is what everyone is actually watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do before the first report
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Agree the three numbers with whoever will read the report, before you collect anything&lt;/li&gt;
&lt;li&gt;Fix the retention period for the report itself (it contains personal data)&lt;/li&gt;
&lt;li&gt;State the aggregation floor — no group smaller than five people&lt;/li&gt;
&lt;li&gt;Decide who can see the raw data versus the report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten minutes of agreement up front saves a year of arguments about methodology.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.imonitorsoft.com/employee-monitoring-software.html" rel="noopener noreferrer"&gt;iMonitor EAM&lt;/a&gt; collects session and application metadata, file operations, USB events and keystroke counts on your own server — enough to produce the three numbers, without the content that turns a management report into an investigation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;General information, not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The fields a monitoring agent should refuse to collect</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:01:53 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/the-fields-a-monitoring-agent-should-refuse-to-collect-2p35</link>
      <guid>https://dev.to/imonitorsoft/the-fields-a-monitoring-agent-should-refuse-to-collect-2p35</guid>
      <description>&lt;p&gt;Most monitoring agents are built by adding features. The hard part is subtraction: deciding what the agent will not collect even though it technically could.&lt;/p&gt;

&lt;p&gt;That list is not a legal nicety. It determines the blast radius of a breach, the amount of data you must disclose, and whether an administrator can defend a decision six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to collect (and why)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it is defensible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session start / end / idle transitions&lt;/td&gt;
&lt;td&gt;Answers "was the desk staffed", no content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application and window titles&lt;/td&gt;
&lt;td&gt;Answers "was the licensed tool used"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File copy / move / print events (path + size)&lt;/td&gt;
&lt;td&gt;Data movement, not content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB insert and file write&lt;/td&gt;
&lt;td&gt;Exfiltration signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keystroke &lt;strong&gt;count&lt;/strong&gt; per interval&lt;/td&gt;
&lt;td&gt;Workload intensity without content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of these can be explained in a sentence to the person being recorded. That is the test: if the explanation needs a paragraph of justification, the field probably should not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to refuse
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typed content.&lt;/strong&gt; Full keystroke logging captures passwords, personal messages, medical information, and everything a customer said. Store counts, not text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clipboard contents.&lt;/strong&gt; It is the single most sensitive buffer on a machine. Store "copy event in app X" if you must, never the payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen recording by default.&lt;/strong&gt; Screenshots on an interval are already intrusive; continuous video multiplies storage by 20–60× and captures whatever happens to be on screen, including unrelated people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webcam and microphone.&lt;/strong&gt; Almost never justified in an employer-employee context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location outside work hours.&lt;/strong&gt; Company device or not, tracking after hours is a different legal animal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal device content.&lt;/strong&gt; If BYOD is allowed, scope to work applications, not the whole machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat and mail bodies.&lt;/strong&gt; A separate module, off by default, with its own retention.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why "we might need it later" is the wrong default
&lt;/h2&gt;

&lt;p&gt;Three concrete costs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Breach scope.&lt;/strong&gt; Content you never collected cannot leak. A breach report that says "session metadata" reads very differently from one that says "keystrokes and clipboard".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disclosure obligations.&lt;/strong&gt; More categories means more jurisdictions where you must notify, and longer incident reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insider risk.&lt;/strong&gt; Every extra field is something an administrator can misuse, and something you must audit access to.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Make the refusal visible in the product
&lt;/h2&gt;

&lt;p&gt;A refusal that lives only in a policy document is not a product decision. Put it in the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;"what we do not record"&lt;/strong&gt; block on the compliance page, in the language of the buyer&lt;/li&gt;
&lt;li&gt;A settings screen where the sensitive modules are off by default and labelled as exceptions&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;audit log&lt;/strong&gt; of who enabled what, with a date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a customer asks "can it capture keystrokes?", the honest answer should be "counts, not content" — and the interface should say so before the salesperson does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exception path
&lt;/h2&gt;

&lt;p&gt;Some organisations genuinely need content-level audit: regulated environments, a specific leak investigation, shared machines handling sensitive data. Those cases need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Written authorisation with a named approver&lt;/li&gt;
&lt;li&gt;A time bound (start and end date)&lt;/li&gt;
&lt;li&gt;Access logging, so the audit is itself auditable&lt;/li&gt;
&lt;li&gt;A removal step when the case closes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a very different posture from a toggle that stays on for the whole company for three years.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test you can run before buying
&lt;/h2&gt;

&lt;p&gt;Ask the vendor for the &lt;strong&gt;field list&lt;/strong&gt; of what the agent sends, then ask them to strike out anything they would not want recorded about themselves. The size of the remaining list tells you what kind of product it is.&lt;/p&gt;




&lt;p&gt;We build &lt;a href="https://www.imonitorsoft.com/employee-monitoring-software.html" rel="noopener noreferrer"&gt;iMonitor EAM&lt;/a&gt; with exactly this list: session and application metadata, file operations, USB events, keystroke counts, on-demand screenshots — and the content modules off by default, on your own server, with a retention job that actually runs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;General information, not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>monitoring</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>A support desk on one small server: sizing, backups and the boring parts</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:59:12 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/a-support-desk-on-one-small-server-sizing-backups-and-the-boring-parts-4nam</link>
      <guid>https://dev.to/imonitorsoft/a-support-desk-on-one-small-server-sizing-backups-and-the-boring-parts-4nam</guid>
      <description>&lt;p&gt;Most "run your own help desk" guides stop at &lt;code&gt;docker compose up&lt;/code&gt;. The interesting part starts three months later, when the disk fills up or a restore has to actually work.&lt;/p&gt;

&lt;p&gt;Here is the unglamorous version, for a team of three to ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually consumes disk
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Growth&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Message bodies + metadata&lt;/td&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;thousands of tickets ≈ a few hundred MB/year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attachments&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;fast&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;screenshots and PDFs dominate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search index&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;rebuildable, can be excluded from backups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;slow but relentless&lt;/td&gt;
&lt;td&gt;cap at 30 days, rotate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database WAL / binlogs&lt;/td&gt;
&lt;td&gt;bursty&lt;/td&gt;
&lt;td&gt;the usual suspect when disk "suddenly" fills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: &lt;strong&gt;plan for attachments to be 10× the database.&lt;/strong&gt; A 2 GB database usually comes with a 20 GB attachment store.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sizing that survives a year
&lt;/h2&gt;

&lt;p&gt;For 5 agents, ~60 tickets/day, average 1.5 MB of attachments per ticket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;60 × 1.5 MB × 365 ≈ 33 GB/year of attachments
database + index         ≈ 3-5 GB/year
logs (capped)            ≈ 2 GB
------------------------------------------
one year                 ≈ 38-40 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: 80 GB of disk with retention pruning, or 200 GB if you keep attachments forever. Both are one cheap VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three cron jobs that matter
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. logical backup, compressed, off the same disk&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  pg_dump &lt;span class="nt"&gt;-Fc&lt;/span&gt; app | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /backup/db-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.dump.gz

&lt;span class="c"&gt;# 2. attachments: rsync to a second location, not a tarball&lt;/span&gt;
30 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; /data/attachments/ /backup/attachments/

&lt;span class="c"&gt;# 3. prune, with a count in the log so you can prove it ran&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  find /backup &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'db-*.dump.gz'&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +14 &lt;span class="nt"&gt;-delete&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that save you later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pg_dump -Fc&lt;/code&gt;&lt;/strong&gt;, not a raw file copy. A copied data directory from a running server is not a backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rsync attachments, do not tarball them.&lt;/strong&gt; Restoring one file should not require unpacking 20 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the count.&lt;/strong&gt; When someone asks "is the backup running?", the log answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The restore drill
&lt;/h2&gt;

&lt;p&gt;Once a quarter, on a scratch machine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an empty database, &lt;code&gt;pg_restore&lt;/code&gt; last night's dump&lt;/li&gt;
&lt;li&gt;Point the app at it, start it&lt;/li&gt;
&lt;li&gt;Open a six-month-old thread, download one attachment, send one test reply&lt;/li&gt;
&lt;li&gt;Note the elapsed time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If that takes more than an hour, your restore procedure is a project, not a procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mail is the part people forget
&lt;/h2&gt;

&lt;p&gt;The app is not the only thing that breaks. If inbound mail flows through the same box, an outage means customers get bounces and you learn about it two days later.&lt;/p&gt;

&lt;p&gt;Two options that both work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MX at an independent provider&lt;/strong&gt;, forwarding into the app over IMAP/SMTP. Mail keeps arriving during app downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTP relay&lt;/strong&gt; for outbound only, so deliverability does not depend on your server's IP reputation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not put inbound mail on a hostname whose DNS lives inside the app server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups are not the same as portability
&lt;/h2&gt;

&lt;p&gt;A database dump restores &lt;em&gt;your&lt;/em&gt; installation. Portability means you can also leave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A per-ticket export (JSON or CSV with bodies and attachment paths)&lt;/li&gt;
&lt;li&gt;Attachments stored under stable, guessable paths&lt;/li&gt;
&lt;li&gt;No proprietary blob format for the search index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test the export once. If it produces a zip you can open on a laptop, you are not locked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not bother with at this size
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes.&lt;/strong&gt; One compose file and a systemd unit is easier to debug at 2 a.m.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read replicas.&lt;/strong&gt; At 10 agents, the database is idle most of the day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object storage for attachments&lt;/strong&gt; unless you already run it — a mounted disk with rsync is fine, and simpler to restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch.&lt;/strong&gt; Postgres full-text search handles a few hundred thousand messages without complaint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The minimum viable runbook
&lt;/h2&gt;

&lt;p&gt;One page, printed and taped inside a cupboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where the server is, and its IP (not only the hostname)&lt;/li&gt;
&lt;li&gt;Where DNS is managed, and who can change it&lt;/li&gt;
&lt;li&gt;How to restart the app, and how to tell it worked&lt;/li&gt;
&lt;li&gt;Where backups land, and how to restore one&lt;/li&gt;
&lt;li&gt;The escalation channel when the app is down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the person who set it up is on holiday, that page is the difference between a 20-minute fix and a lost day.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://onsitesai.com/" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; ships as a compose stack — app, Postgres, queue worker, optional mail fetcher — with the &lt;a href="https://onsitesai.com/self-host.html" rel="noopener noreferrer"&gt;self-hosting notes&lt;/a&gt; covering exactly the parts above. Free for the first three seats if you want to measure your own ticket volume before sizing anything.&lt;/p&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>When your vendor vanishes: a practical checklist for owning your support data</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:55:57 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/when-your-vendor-vanishes-a-practical-checklist-for-owning-your-support-data-1c36</link>
      <guid>https://dev.to/imonitorsoft/when-your-vendor-vanishes-a-practical-checklist-for-owning-your-support-data-1c36</guid>
      <description>&lt;p&gt;A vendor outage is annoying. A vendor &lt;em&gt;disappearing&lt;/em&gt; is a business event. The difference is not how long the downtime lasts — it is whether you can get your data and your domain back.&lt;/p&gt;

&lt;p&gt;Here is the checklist I would run against any tool that holds customer conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Can you export everything, in a format you can read?
&lt;/h2&gt;

&lt;p&gt;Not "we support export" — ask for a sample file. Three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it include &lt;strong&gt;message bodies&lt;/strong&gt; or only metadata?&lt;/li&gt;
&lt;li&gt;Does it preserve &lt;strong&gt;attachments&lt;/strong&gt;, or just filenames?&lt;/li&gt;
&lt;li&gt;Does it preserve &lt;strong&gt;threading&lt;/strong&gt; (which message answered which)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A CSV of ticket titles is not an export. If the demo cannot produce a zip with bodies, attachment files and thread structure, assume you cannot leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Is your domain yours, independently?
&lt;/h2&gt;

&lt;p&gt;The single most common failure: the domain's nameservers are hosted &lt;strong&gt;inside the same infrastructure as the product&lt;/strong&gt;. When that box dies, DNS dies, so mail bounces and you cannot even re-point the domain to a new host — you have no reachable nameserver to edit.&lt;/p&gt;

&lt;p&gt;Check in 30 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short NS yourdomain.com
&lt;span class="k"&gt;for &lt;/span&gt;ns &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;dig +short NS yourdomain.com&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ns&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short A &lt;span class="nv"&gt;$ns&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all nameservers share one IP, that IP is a single point of failure for your entire company, not just the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Can you still read your mail if the tool is down?
&lt;/h2&gt;

&lt;p&gt;Support mail usually lands in the tool. If mail delivery depends on the tool's MX records, an outage means customers get bounces — and you find out days later from a customer who gave up.&lt;/p&gt;

&lt;p&gt;Keeping MX at an independent provider (or at least a provider you control separately) means mail keeps arriving while the app is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What is your break-glass path?
&lt;/h2&gt;

&lt;p&gt;Write it down, once, on one page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where the origin IP is recorded (not only in DNS)&lt;/li&gt;
&lt;li&gt;Who can change nameservers, and where that login lives&lt;/li&gt;
&lt;li&gt;The last known-good export, and its date&lt;/li&gt;
&lt;li&gt;Which channel you use to tell customers something is down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have watched teams spend the first four hours of an outage looking for credentials. That is not a technical problem; it is a documentation problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Does the contract survive the vendor?
&lt;/h2&gt;

&lt;p&gt;Ask for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stated &lt;strong&gt;export format&lt;/strong&gt; and how long after cancellation it stays available&lt;/li&gt;
&lt;li&gt;Notice period for shutdown or migration&lt;/li&gt;
&lt;li&gt;Data deletion timeline, and confirmation they will not delete on day one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of that is in writing, you are relying on the vendor's goodwill at the exact moment they are least able to provide it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Self-hosting is a spectrum, not a binary
&lt;/h2&gt;

&lt;p&gt;Some teams need the whole stack on their own hardware. Most just need the &lt;em&gt;data&lt;/em&gt; to be portable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What you run&lt;/th&gt;
&lt;th&gt;What you keep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;td&gt;Export + domain + mail DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;App in cloud, data exports nightly&lt;/td&gt;
&lt;td&gt;Database + attachments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;App, database and storage&lt;/td&gt;
&lt;td&gt;Everything, including DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle option is underrated. You keep the convenience and remove the lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test the restore, not the backup
&lt;/h2&gt;

&lt;p&gt;A backup you have never restored is a hope. Once a quarter: export from the tool, import into a scratch environment, open a two-year-old thread, check one attachment opens.&lt;/p&gt;

&lt;p&gt;Ten minutes, and it converts an untested assumption into a fact.&lt;/p&gt;




&lt;p&gt;We build &lt;a href="https://onsitesai.com/" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; as a shared inbox with a Docker Compose deployment: app, database and queue on your own hardware, mail through any provider you like. The &lt;a href="https://onsitesai.com/self-host.html" rel="noopener noreferrer"&gt;self-hosting notes&lt;/a&gt; are deliberately boring — they list what to prepare before the first install, including the parts above that decide whether a bad day becomes a lost quarter.&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>management</category>
      <category>saas</category>
    </item>
    <item>
      <title>Two nameservers on one box: the outage that takes DNS and the website down together</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:50:17 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/two-nameservers-on-one-box-the-outage-that-takes-dns-and-the-website-down-together-4dbg</link>
      <guid>https://dev.to/imonitorsoft/two-nameservers-on-one-box-the-outage-that-takes-dns-and-the-website-down-together-4dbg</guid>
      <description>&lt;p&gt;A domain goes offline in two very different ways, and most small teams have only planned for one of them.&lt;/p&gt;

&lt;p&gt;The first: the web server dies. DNS still resolves, browsers get a connection refused, someone restarts the box, everyone moves on.&lt;/p&gt;

&lt;p&gt;The second: the nameservers die. Nothing resolves at all. Mail bounces, the marketing site disappears, the API looks like it never existed, and — worst of all — you cannot even reach the server to fix it, because you have no way to find it.&lt;/p&gt;

&lt;p&gt;The second failure mode is entirely avoidable, and it is usually invisible until it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The configuration that causes it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;imonitorsoft.com.   NS   ns1.imonitorsoft.com.
imonitorsoft.com.   NS   ns2.imonitorsoft.com.
ns1.imonitorsoft.com. A  203.0.113.10
ns2.imonitorsoft.com. A  203.0.113.10   &amp;lt;-- same address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two nameservers, one IP. Registrars accept this. It satisfies the "at least two NS records" checkbox. It provides exactly zero redundancy, because both nameservers are the same machine, and usually the same machine as the website.&lt;/p&gt;

&lt;p&gt;When that box stops answering, the entire zone becomes SERVFAIL. Not "down" in the sense your monitoring notices — the domain effectively stops existing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check yours in 60 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# what the registry delegates to&lt;/span&gt;
dig +short NS example.com

&lt;span class="c"&gt;# where those nameservers actually live&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;ns &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;dig +short NS example.com&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ns&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short A &lt;span class="nv"&gt;$ns&lt;/span&gt; | &lt;span class="nb"&gt;paste&lt;/span&gt; &lt;span class="nt"&gt;-sd&lt;/span&gt;, -&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# and ask a public resolver, not your own cache&lt;/span&gt;
dig @1.1.1.1 example.com A +short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every nameserver resolves to the same IP, you have a single point of failure that will take the whole domain with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in order of effort
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Move DNS to a provider that runs it as a product.&lt;/strong&gt; Cloudflare's free tier is enough. You point the registrar at their nameservers and never host authoritative DNS on the same box as the website again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. If you must self-host DNS, split it.&lt;/strong&gt; Two nameservers on two providers (or two regions), each with its own address. The registry's requirement exists precisely because one machine is not redundancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Keep the DNS provider independent of the hosting provider.&lt;/strong&gt; When your host has a bad day, you want to be able to re-point the A record somewhere else in five minutes. If DNS lives inside the host, you cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs when it goes wrong
&lt;/h2&gt;

&lt;p&gt;Not the server bill — the recovery time. When the domain does not resolve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You cannot SSH by name; you need the IP written down somewhere&lt;/li&gt;
&lt;li&gt;Customers cannot reach support to tell you the site is down&lt;/li&gt;
&lt;li&gt;Certificate renewals and API callbacks fail silently&lt;/li&gt;
&lt;li&gt;Search engines start dropping pages from the index after a few days&lt;/li&gt;
&lt;li&gt;Every link you published for months points at a host that no longer answers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the expensive part. Content is recoverable — you have the source. Index position is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Nameservers resolve to at least two different addresses&lt;/li&gt;
&lt;li&gt;[ ] DNS provider is not your hosting provider&lt;/li&gt;
&lt;li&gt;[ ] The origin IP is written down somewhere outside the infrastructure&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;dig @1.1.1.1 yourdomain.com&lt;/code&gt; works from a network you do not control&lt;/li&gt;
&lt;li&gt;[ ] You have a copy of the zone file exported somewhere&lt;/li&gt;
&lt;li&gt;[ ] Monitoring checks resolution, not just HTTP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last item matters more than it looks. An HTTP check on a hostname silently fails when DNS fails — and if your monitor runs inside the same network, its cache may still answer, so it reports green while the world sees SERVFAIL.&lt;/p&gt;




&lt;p&gt;We run &lt;a href="https://onsitesai.com/" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; with a self-hosted option, and this is exactly why the deployment docs insist on external DNS rather than putting it on the app server. If you are setting up a small support stack, the &lt;a href="https://onsitesai.com/self-host.html" rel="noopener noreferrer"&gt;self-hosting notes&lt;/a&gt; cover the boring parts that decide whether you survive a bad day.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>networking</category>
    </item>
    <item>
      <title>What an on-premise monitoring server actually stores (and how to size the disk)</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Thu, 29 Oct 2026 06:46:38 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/what-an-on-premise-monitoring-server-actually-stores-and-how-to-size-the-disk-48ee</link>
      <guid>https://dev.to/imonitorsoft/what-an-on-premise-monitoring-server-actually-stores-and-how-to-size-the-disk-48ee</guid>
      <description>&lt;p&gt;Every on-premise monitoring pitch ends with the same question from IT: "fine, but how much disk?" The honest answer is a formula, not a number. Here is what actually lands on the server and how to estimate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets stored
&lt;/h2&gt;

&lt;p&gt;A typical deployment writes five kinds of rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;One row per&lt;/th&gt;
&lt;th&gt;Typical size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;login / logout / idle transition&lt;/td&gt;
&lt;td&gt;~200 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app_usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;app or site, aggregated per interval&lt;/td&gt;
&lt;td&gt;~150 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file_events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;copy, move, delete, print, USB write&lt;/td&gt;
&lt;td&gt;~400 bytes + path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;screenshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one captured frame&lt;/td&gt;
&lt;td&gt;150–400 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;audit_log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;who viewed which recording&lt;/td&gt;
&lt;td&gt;~200 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three are metadata. The fourth is the only thing that grows fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sizing formula
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;disk ≈ endpoints × captures/day × avg_capture_size × retention_days
     + endpoints × metadata_per_endpoint_per_year × years
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A concrete case — 200 endpoints, one capture every 10 minutes (≈48/day), 250 KB average, 30 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200 × 48 × 250 KB × 30 ≈ 72 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metadata for the same fleet is a rounding error by comparison: tens of GB per year, not hundreds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video changes the order of magnitude
&lt;/h2&gt;

&lt;p&gt;Screen &lt;em&gt;recording&lt;/em&gt;, if you enable it, costs 20–60× the equivalent duration in screenshots depending on resolution and codec. Two consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable recording per-group, not per-tenant. Most organisations need it for a handful of regulated roles.&lt;/li&gt;
&lt;li&gt;Cap recording duration in the policy, not in the storage layer. Disk-full is a much worse failure than "recording stopped at the limit".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Retention must be enforced by the system
&lt;/h2&gt;

&lt;p&gt;A retention policy in a PDF is not retention. The database needs a scheduled delete with three properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-table windows&lt;/strong&gt; — screenshots expire faster than file events, which expire faster than payroll-relevant time records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal hold override&lt;/strong&gt; — one flag on a case that suspends deletion for exactly those records, with an owner and an end date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dry-run mode&lt;/strong&gt; — so you can see what &lt;em&gt;would&lt;/em&gt; be deleted before turning it on.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- pseudocode for the nightly job&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;screenshots&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;captured_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;legal_holds&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;released_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;LIMIT&lt;/code&gt; batches during business hours, and log the count. The log is what proves the policy is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups are the hidden doubling
&lt;/h2&gt;

&lt;p&gt;The estimate above is live data. Backups typically double it, and a second copy off-site doubles it again. If the number you quoted to management was 72 GB, the actual infrastructure number is closer to 150–200 GB once retention snapshots exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network is rarely the bottleneck
&lt;/h2&gt;

&lt;p&gt;Agents that only send metadata use almost nothing. Screenshots are compressed and sent on intervals. The real spike happens when an administrator opens a playback session — that is a burst of image reads, not sustained WAN traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things worth deciding early
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where does the database live?&lt;/strong&gt; On the same box as the screenshot store, you get one failure domain and one restore procedure. Separate them and you get scale, plus two things to back up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the growth assumption?&lt;/strong&gt; Sizing for today's endpoint count is the classic mistake; size for the fleet you expect when the licence renewal comes around.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This is the model iMonitor EAM ships with: server and database on your own hardware, keystroke &lt;strong&gt;counts&lt;/strong&gt; rather than content, per-module retention, and a 15-day trial that is enough to measure your own capture rate before committing to disk. More at &lt;a href="https://www.imonitorsoft.com/employee-monitoring-software.html" rel="noopener noreferrer"&gt;imonitorsoft.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>infrastructure</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Modelling a shared inbox: why the message is the primary key, not the conversation</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Thu, 29 Oct 2026 04:53:34 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/modelling-a-shared-inbox-why-the-message-is-the-primary-key-not-the-conversation-3cjn</link>
      <guid>https://dev.to/imonitorsoft/modelling-a-shared-inbox-why-the-message-is-the-primary-key-not-the-conversation-3cjn</guid>
      <description>&lt;p&gt;Every shared-inbox implementation starts with a &lt;code&gt;conversations&lt;/code&gt; table and ends up regretting it. Here is the model that survives contact with real support traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with conversation-first
&lt;/h2&gt;

&lt;p&gt;A conversation is a derived thing. It is what you get &lt;em&gt;after&lt;/em&gt; you decide which messages belong together. If you store conversations as the source of truth, every new channel forces you to write merge logic that mutates the conversation rows — and merges are the one operation that must never lose data.&lt;/p&gt;

&lt;p&gt;Messages, by contrast, arrive once and never change. They are the natural primary key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- the immutable fact&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sent_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- who it came from (may be shared by many messages)&lt;/span&gt;
&lt;span class="n"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- the derived grouping, rebuildable at any time&lt;/span&gt;
&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opened_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closed_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- the link, append-only&lt;/span&gt;
&lt;span class="n"&gt;thread_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;added_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;thread_messages&lt;/code&gt; is append-only.&lt;/strong&gt; A merge inserts rows; it never rewrites history. If the merge was wrong, you can split by deleting the link rows — no data lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threads can be rebuilt.&lt;/strong&gt; Delete every row in &lt;code&gt;threads&lt;/code&gt; and you can regenerate them from messages plus your merge rules. That is the test for whether your grouping logic is a projection or a tangle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reason&lt;/code&gt; is stored.&lt;/strong&gt; "Same contact, 4h window" vs "manual merge by ana" answers the question every support lead eventually asks: why are these two emails in one thread?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Identity is the hard part
&lt;/h2&gt;

&lt;p&gt;The same person writes from &lt;code&gt;ana@company.com&lt;/code&gt;, then from a personal address, then from a second work address after a rebrand. Matching on the exact string gives you three contacts and three threads.&lt;/p&gt;

&lt;p&gt;A workable compromise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exact match first (email, phone, then channel-specific ID)&lt;/li&gt;
&lt;li&gt;Then a domain-and-name heuristic that &lt;em&gt;suggests&lt;/em&gt; a merge rather than performing one&lt;/li&gt;
&lt;li&gt;Never auto-merge across organisations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The suggestion queue is boring, and it is what keeps identity resolution honest: a human confirms the ambiguous cases rather than a regex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery state belongs to the message, not the conversation
&lt;/h2&gt;

&lt;p&gt;Outbound mail can be &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;bounced&lt;/code&gt;, &lt;code&gt;complained&lt;/code&gt;. That lifecycle is per message and per recipient. Storing "the conversation was sent" hides the one case that matters: one of three recipients bounced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;message_delivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that table, "did they get it?" is a query instead of a support escalation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Channel integrations become writers of &lt;code&gt;messages&lt;/code&gt;, not reimplementations of threading&lt;/li&gt;
&lt;li&gt;AI summarisation or tagging attaches to messages/threads as a separate table, never mutating the record&lt;/li&gt;
&lt;li&gt;Deleting a thread is a policy operation (remove link rows + messages), so retention rules become enforceable&lt;/li&gt;
&lt;li&gt;Migration to a new database is a row copy, not a logic rewrite&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one thing to get right early
&lt;/h2&gt;

&lt;p&gt;Make &lt;code&gt;messages.external_id&lt;/code&gt; unique per channel and dedupe on insert. Every provider retries webhooks. Without that constraint you will ship duplicate replies, and duplicate replies are the failure customers remember.&lt;/p&gt;




&lt;p&gt;This is the shape we use in &lt;a href="https://onsitesai.com/" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; — a shared inbox, CRM and light invoicing for teams of three to thirty, self-hostable or cloud. The schema notes above are portable either way.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Self-hosting a support desk: what actually changes when customer data cannot leave the building</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Wed, 28 Oct 2026 17:44:13 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/self-hosting-a-support-desk-what-actually-changes-when-customer-data-cannot-leave-the-building-5730</link>
      <guid>https://dev.to/imonitorsoft/self-hosting-a-support-desk-what-actually-changes-when-customer-data-cannot-leave-the-building-5730</guid>
      <description>&lt;p&gt;"Can we self-host it?" is usually asked late, after a team has already started using a cloud tool. The answer changes more than the deployment diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Backups become your problem, and that is the point
&lt;/h2&gt;

&lt;p&gt;In a hosted product, backups are a line in a contract. Self-hosted, they are a cron job, an off-site copy, and a restore you have actually tested. Teams that skip the restore test discover the problem at the worst moment.&lt;/p&gt;

&lt;p&gt;The upside: the backup never leaves infrastructure you control, which is often the original reason for self-hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Upgrades are a decision, not an event
&lt;/h2&gt;

&lt;p&gt;Cloud products upgrade on the vendor's schedule. Self-hosted, an upgrade is a maintenance window you choose — and can postpone during a busy week. That flexibility is real, and so is the cost: someone has to read the changelog.&lt;/p&gt;

&lt;p&gt;A practical rule: pin the image tag, upgrade monthly, keep the previous tag for a one-command rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Attachments change the storage question
&lt;/h2&gt;

&lt;p&gt;Conversations carry files. In the cloud, those files sit in someone else's object storage. Self-hosted, they sit on your disk or your S3-compatible bucket, which means retention, quotas and encryption are yours to define — and yours to get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Email is the hard dependency
&lt;/h2&gt;

&lt;p&gt;A support desk that cannot send email is not a support desk. Self-hosting the app is easy; self-hosting reliable outbound email is not. Most teams run the app themselves and still send through a transactional provider. That is a reasonable split: the conversations stay inside, the delivery goes through a specialist.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What you actually gain
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A retention policy you can enforce rather than request&lt;/li&gt;
&lt;li&gt;Data residency answered with an IP address instead of a document&lt;/li&gt;
&lt;li&gt;Access control that maps to your own identity provider&lt;/li&gt;
&lt;li&gt;The ability to run on a network with no outbound internet, if that is the requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. What you actually give up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Vendor-managed uptime&lt;/li&gt;
&lt;li&gt;Someone else testing upgrades before you see them&lt;/li&gt;
&lt;li&gt;Support that assumes they can see your database&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A workable middle ground
&lt;/h2&gt;

&lt;p&gt;Run the app in Docker with three services (app, database, queue), a nightly dump to an off-site bucket, and outbound email through a transactional provider. That covers most self-hosting requirements without turning into a platform team.&lt;/p&gt;




&lt;p&gt;This is the model &lt;a href="https://onsitesai.com/self-host.html" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; ships with: a Docker Compose deployment for the shared inbox, CRM and invoicing, with cloud available for teams that would rather not run it. Free for the first three seats either way.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Telegram, LINE and WeChat webhooks are not like email: notes from wiring six channels into one queue</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Wed, 28 Oct 2026 16:45:23 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/telegram-line-and-wechat-webhooks-are-not-like-email-notes-from-wiring-six-channels-into-one-queue-4gg9</link>
      <guid>https://dev.to/imonitorsoft/telegram-line-and-wechat-webhooks-are-not-like-email-notes-from-wiring-six-channels-into-one-queue-4gg9</guid>
      <description>&lt;p&gt;Email is forgiving. A webhook from a messaging platform is not. Here is what actually differs when you put Telegram, LINE, WeChat, WhatsApp and a website widget behind one inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Each platform answers "did it arrive?" differently
&lt;/h2&gt;

&lt;p&gt;Email has bounces. WhatsApp has receipts. LINE has its own delivery semantics. Telegram tells you the message was sent to the Bot API and nothing more. The website widget has no delivery state at all — a successful HTTP response is the whole story.&lt;/p&gt;

&lt;p&gt;Modelling one status enum across all of them produces a field that means nothing. Keep the provider's native state and a small normalised enum used only to decide whether the UI may say "delivered".&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Retries are guaranteed, ordering is not
&lt;/h2&gt;

&lt;p&gt;Webhooks retry on timeout. Mobile clients queue and flush. A conversation arriving from a device that was offline will not be in timestamp order, and it will not arrive once.&lt;/p&gt;

&lt;p&gt;Two things make this survivable: an idempotency key taken from the provider on every inbound event, and an ordering key stored alongside the message so the UI sorts on provider sequence rather than arrival time.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Identity is the hard part, not transport
&lt;/h2&gt;

&lt;p&gt;The same person writes on WhatsApp, then emails, then opens the widget. Three identities, one human.&lt;/p&gt;

&lt;p&gt;Resolve identity on ingest (same phone, same verified email), not on read. The cost is a merge operation when two channel identities turn out to be the same person later — which is testable — instead of reconciling three histories at query time, which is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Media is a liability, not a feature
&lt;/h2&gt;

&lt;p&gt;Attachments arrive as either a URL with an expiry, a base64 blob, or a provider-hosted id that needs a second call. All three need to be mirrored into your own storage on ingest, with size limits and type checks, or your inbox breaks silently six months later when the provider rotates its CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Rate limits shape the UI
&lt;/h2&gt;

&lt;p&gt;Some platforms cap outbound messages per second per account. If the composer does not know that, an agent replying to a burst gets a silent failure. Surface the queue depth per channel, not a global spinner.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What we deliberately did not build
&lt;/h2&gt;

&lt;p&gt;No workflow builder, no plugin marketplace. Six channels, one queue, three triage primitives (tags, priority, views), and AI drafts that live inside the conversation so a human can take over mid-thread.&lt;/p&gt;




&lt;p&gt;The product this went into is &lt;a href="https://onsitesai.com/" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; — a shared inbox across website chat, email, Telegram, WhatsApp, Messenger, LINE and WeChat, with CRM and invoicing attached, free for the first three seats.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Routing seven messaging channels into one queue: what actually decides the design</title>
      <dc:creator>iMonitor Software</dc:creator>
      <pubDate>Wed, 28 Oct 2026 16:08:38 +0000</pubDate>
      <link>https://dev.to/imonitorsoft/routing-seven-messaging-channels-into-one-queue-what-actually-decides-the-design-hl5</link>
      <guid>https://dev.to/imonitorsoft/routing-seven-messaging-channels-into-one-queue-what-actually-decides-the-design-hl5</guid>
      <description>&lt;p&gt;Most "omnichannel support" discussions are about which channels a product supports. The harder part is the data model behind them: what a conversation &lt;em&gt;is&lt;/em&gt; when the same person writes on WhatsApp, then emails, then opens the chat widget.&lt;/p&gt;

&lt;p&gt;Here are the four decisions that shaped ours, and the trade-offs behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A conversation belongs to a customer, not to a channel
&lt;/h2&gt;

&lt;p&gt;The intuitive model is one table per channel: &lt;code&gt;whatsapp_messages\&lt;/code&gt;, &lt;code&gt;email_threads\&lt;/code&gt;, &lt;code&gt;chat_sessions\&lt;/code&gt;. It is easy to build and painful to live with. The moment a customer switches channel, the history splits, and the agent has to remember that "the person who emailed us" is also "the WhatsApp number from yesterday".&lt;/p&gt;

&lt;p&gt;We inverted it: a conversation is an entity with a channel-agnostic identity, and each channel is an adapter that writes into it. Identity resolution (same phone, same email) happens on ingest, not on read.&lt;/p&gt;

&lt;p&gt;The cost is a merge step: two channel identities can turn out to be one person after the fact. That is a normal, testable operation — much cheaper than reconciling three histories at query time.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Delivery state is per channel, not per message
&lt;/h2&gt;

&lt;p&gt;Email has &lt;code&gt;SENT\&lt;/code&gt;, &lt;code&gt;DELIVERED\&lt;/code&gt;, &lt;code&gt;BOUNCED\&lt;/code&gt;. WhatsApp has its own receipt semantics, LINE another, and the web widget has none of them — the "delivery" is just a successful HTTP response.&lt;/p&gt;

&lt;p&gt;If you model one status enum for all channels, you end up with a lowest-common-denominator field that means nothing. We keep the provider's native state alongside a small normalised enum used only for UI decisions (is it safe to show "delivered"?).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Out-of-order ingestion is the default, not the exception
&lt;/h2&gt;

&lt;p&gt;Webhooks retry. Mobile clients queue and flush. A conversation arriving from a laptop that was offline will not be in timestamp order.&lt;/p&gt;

&lt;p&gt;Two things made this survivable: an idempotency key from the provider on every inbound event, and an ordering key (provider message id or a monotonic sequence) stored with the message, so the UI sorts on delivery rather than on arrival.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. AI needs to be inside the conversation, not beside it
&lt;/h2&gt;

&lt;p&gt;The tempting design is a separate AI service that reads a copy of the conversation and posts a suggestion. It demos well and fails the audit: the reply lives in a log nobody reads, and a human taking over cannot see what the assistant already told the customer.&lt;/p&gt;

&lt;p&gt;We attach generated drafts to the conversation itself. Any agent can take over mid-thread, the draft is visible in place, and billing is per action rather than per seat — so cost tracks work rather than headcount.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did not build
&lt;/h2&gt;

&lt;p&gt;No workflow builder. Three primitives (tags, priority, views) cover triage for a team of three to ten, and every branch-based builder we have seen at this size gets abandoned in a month. No plugin marketplace either — the seven channels we support are the ones small teams actually receive messages on.&lt;/p&gt;




&lt;p&gt;The product doing this is &lt;a href="https://onsitesai.com/" rel="noopener noreferrer"&gt;Onsites AI&lt;/a&gt; — free for the first three seats, cloud or self-hosted. If you want the operational side rather than the architecture, the &lt;a href="https://onsitesai.blogspot.com/2026/10/the-small-team-support-desk-complete.html" rel="noopener noreferrer"&gt;small-team support desk guide&lt;/a&gt; covers the decisions in order.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
