<?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: Adam Ben-Gur</title>
    <description>The latest articles on DEV Community by Adam Ben-Gur (@adambg).</description>
    <link>https://dev.to/adambg</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%2F2641283%2F6f251841-3c29-484a-aac2-d060c88e1dbd.png</url>
      <title>DEV Community: Adam Ben-Gur</title>
      <link>https://dev.to/adambg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adambg"/>
    <language>en</language>
    <item>
      <title>What Actually Happens When a Domain Gets Hijacked (and why I built a Go service to watch for it)</title>
      <dc:creator>Adam Ben-Gur</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:02:32 +0000</pubDate>
      <link>https://dev.to/adambg/what-actually-happens-when-a-domain-gets-hijacked-and-why-i-built-a-go-service-to-watch-for-it-262k</link>
      <guid>https://dev.to/adambg/what-actually-happens-when-a-domain-gets-hijacked-and-why-i-built-a-go-service-to-watch-for-it-262k</guid>
      <description>&lt;p&gt;A client's domain got hijacked on a Friday night. Nameservers changed, MX records pointed somewhere new, and nobody on our side noticed until email stopped working - which, for a business, is a special kind of silence. No bounce, no error. Messages just went into a void that used to be a mail server.&lt;/p&gt;

&lt;p&gt;By the time we got it sorted out, the honest question wasn't "how do we fix this one domain." It was "how many other domains are we one silent DNS change away from losing, and how would we even know?"&lt;/p&gt;

&lt;p&gt;That question turned into Expirity - a monitoring service that watches domain expiry, DNS records, nameservers, and SSL certs, and tells you the moment something changes instead of the moment you notice. This post is about the part I actually enjoyed building: the checker engine that makes that possible, and a couple of the deceptively tricky problems that came with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;"Monitor a domain" sounds like a cron job that hits WHOIS once a day. In practice it's three separate, slow, unreliable data sources that need to agree with each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registration data&lt;/strong&gt; - who owns it, when it expires, who the registrar is. RDAP is the modern, structured successor to WHOIS, but coverage isn't universal yet - plenty of TLDs still only speak WHOIS's decades-old semi-structured text format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS records&lt;/strong&gt; - A, AAAA, MX, CNAME, TXT, NS, CAA, SOA, plus the derived stuff like SPF/DMARC (which just live inside TXT records but deserve their own change-detection logic because they matter differently) and DNSSEC status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS certificate state&lt;/strong&gt; - issuer, expiry, chain validity, SANs - which is really a separate check against port 443, unrelated to DNS or registration entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these three sources update on the same schedule, none of them are guaranteed to respond, and only one of them (DNS) is fast enough to check every domain every hour without annoying anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  RDAP first, WHOIS as the fallback
&lt;/h2&gt;

&lt;p&gt;The naive version of this is "just parse WHOIS." The problem is that WHOIS was never a real protocol - it's closer to a gentleman's agreement that everyone would return &lt;em&gt;vaguely&lt;/em&gt; similar plain text, and every registry formats that text slightly differently. Parsing it reliably across 1,500+ TLDs means maintaining a small library of regexes that breaks every time a registry redesigns their output.&lt;/p&gt;

&lt;p&gt;RDAP (Registration Data Access Protocol) fixes this - it's JSON, it's structured, it's an actual IETF standard. The catch is that RDAP server coverage, while growing, still isn't universal. So the checker does the obvious thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;lookupExpiry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ExpiryData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdapLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// No RDAP server for this TLD, or it didn't respond - fall back&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;whoisLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple in outline, less simple in practice - WHOIS fallback needs its own timeout handling, its own rate-limit backoff per registry, and its own "I got a response but I'm not confident I parsed it correctly" signal so a bad parse doesn't get treated as "domain has no expiry date," which is a very different (and alarming) thing to tell a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change-detection problem nobody warns you about
&lt;/h2&gt;

&lt;p&gt;The actual hard part isn't checking a domain once. It's checking it every day (or every hour, depending on plan) and correctly identifying &lt;em&gt;what changed since last time&lt;/em&gt; - without false-positiving on noise or, worse, silently missing a real change.&lt;/p&gt;

&lt;p&gt;The approach: every check produces a snapshot (expiry date, registrar, nameservers, SSL fingerprint, DNS record set). Each new snapshot gets diffed against the last one, and every difference becomes an explicit, typed event - &lt;code&gt;nameserver_changed&lt;/code&gt;, &lt;code&gt;ssl_issuer_changed&lt;/code&gt;, &lt;code&gt;dns_record_added&lt;/code&gt;, and so on - rather than just overwriting the old value and hoping someone happens to look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ChangeEvent&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DomainID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;OccurredAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;EventType&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="c"&gt;// "nameserver_changed", "dns_record_added", ...&lt;/span&gt;
    &lt;span class="n"&gt;Field&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;OldValue&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;NewValue&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds obvious in retrospect, but it's the entire point: an "expiry date" field that just gets silently updated in place tells you nothing. A &lt;code&gt;nameserver_changed&lt;/code&gt; event with the old and new values, timestamped, alertable, and queryable six months later - that's the thing that would have caught the Friday-night hijack before it became a support call.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first-check trap
&lt;/h3&gt;

&lt;p&gt;Here's the bug I didn't see coming. The very first time you check a &lt;em&gt;brand new&lt;/em&gt; domain, there's no previous snapshot to diff against. Naive code will happily treat "domain has 6 DNS records" as "6 records were just added" and fire six change events and six alerts, for a domain someone added ten seconds ago on purpose.&lt;/p&gt;

&lt;p&gt;The fix is a deliberate sentinel: &lt;code&gt;lastCheckedAt == nil&lt;/code&gt; means "this is the baseline, not a change," so the first check populates the snapshot silently and only starts diffing from the &lt;em&gt;second&lt;/em&gt; check onward. It's a two-line guard clause, but it's the difference between "useful alerts" and "customer immediately mutes all notifications because day one was a spam storm."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastCheckedAt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// First check: record the baseline, emit nothing.&lt;/span&gt;
    &lt;span class="c"&gt;// Everything from here on is a genuine diff.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;persistBaseline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;diffAndEmitEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why two separate services, not one
&lt;/h2&gt;

&lt;p&gt;Expirity is two codebases talking to one MongoDB database: a PHP console (auth, dashboard, billing, all the human-facing stuff) and a Go service that does nothing but check domains and write results. The PHP side is explicitly forbidden from making live WHOIS/SSL/DNS calls - it only ever reads what Go already wrote, plus one narrow &lt;code&gt;POST /check&lt;/code&gt; endpoint on loopback for "check this domain right now" button clicks.&lt;/p&gt;

&lt;p&gt;The reasoning: a web request thread has no business blocking on a WHOIS server in Vanuatu that might take 12 seconds to respond or might not respond at all. Keeping the slow, unreliable, external-network-dependent work in a separate process with its own worker pool, timeouts, and retry logic means a flaky registrar server degrades &lt;em&gt;that one check&lt;/em&gt;, not the dashboard someone's currently looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building the same thing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat "no response" and "no data" as different failure modes.&lt;/strong&gt; A WHOIS server timing out is not the same fact as a domain having no expiry date, and conflating them will eventually tell a customer something false with total confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff explicitly, don't overwrite silently.&lt;/strong&gt; If your monitoring tool's core value is "tell me when something changes," the change itself needs to be a first-class, stored, queryable thing — not an inference someone has to make by comparing two timestamps in a UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard your baseline case.&lt;/strong&gt; Anything that diffs against history will misfire gloriously on the very first run if you don't special-case it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of this sounds like a problem you've also hit — WHOIS parsing hell, alert-storm-on-day-one, or the general fun of "monitor something you don't control the schedule of" — I'd genuinely like to compare notes in the comments.&lt;/p&gt;

&lt;p&gt;And if you're the kind of person who's ever found out a domain expired from a client instead of a monitor: &lt;a href="https://expirity.io" rel="noopener noreferrer"&gt;Expirity&lt;/a&gt; does exactly what's described above, free for up to 50 domains, no credit card required.&lt;/p&gt;

</description>
      <category>golang</category>
      <category>php</category>
      <category>buildinpublic</category>
      <category>mongodb</category>
    </item>
    <item>
      <title>Get Your Emails on WhatsApp</title>
      <dc:creator>Adam Ben-Gur</dc:creator>
      <pubDate>Wed, 12 Mar 2025 13:50:01 +0000</pubDate>
      <link>https://dev.to/adambg/get-your-emails-on-whatsapp-58of</link>
      <guid>https://dev.to/adambg/get-your-emails-on-whatsapp-58of</guid>
      <description>&lt;p&gt;Ever missed an important email because you are getting too much emails? BeepMate solves this problem by sending your (important) emails directly to WhatsApp. Check out &lt;a href="https://beepmate.io" rel="noopener noreferrer"&gt;https://beepmate.io&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is BeepMate?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;BeepMate gives you a special email address (like &lt;code&gt;yournumber@beepmate.io&lt;/code&gt;) that forwards messages to your WhatsApp&lt;/li&gt;
&lt;li&gt;Any attachments also get sent to your WhatsApp&lt;/li&gt;
&lt;li&gt;You can use it for free (limited to 15 messages daily) or upgrade for 
more features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Main Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stay in the loop: Get important emails instantly on your phone&lt;/li&gt;
&lt;li&gt;No app switching: Read emails where you already chat&lt;/li&gt;
&lt;li&gt;Team notifications: Forward alerts to WhatsApp group so everyone sees them&lt;/li&gt;
&lt;li&gt;Simple setup: Takes just a few seconds to get started&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Uses I've Found Helpful
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Business owners:&lt;/strong&gt; Get notified immediately when you receive a new order&lt;br&gt;
&lt;strong&gt;IT teams:&lt;/strong&gt; Send server alerts to a WhatsApp group so problems get fixed faster&lt;br&gt;
&lt;strong&gt;Freelancers:&lt;/strong&gt; Know right away when a client emails you&lt;br&gt;
&lt;strong&gt;Online shoppers:&lt;/strong&gt; Get shipping notifications without checking email&lt;/p&gt;

&lt;h2&gt;
  
  
  Cool Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Send emails to a WhatsApp group (great for team updates)&lt;/li&gt;
&lt;li&gt;Connect your Gmail account to forward specific emails&lt;/li&gt;
&lt;li&gt;Use their API to build custom notifications&lt;/li&gt;
&lt;li&gt;AI summaries of long emails (on paid plans)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BeepMate isn't trying to replace your email - it just puts the important stuff where you'll see it right away. I've found it especially useful for time-sensitive messages that I can't afford to miss.&lt;/p&gt;

&lt;p&gt;The free version works well for basic needs, while the paid version adds features like group notifications and higher daily limits.&lt;/p&gt;

&lt;p&gt;If you're tired of constantly checking your email but don't want to miss the important stuff, BeepMate offers a simple solution that just works.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
