<?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: Haimanot Getu</title>
    <description>The latest articles on DEV Community by Haimanot Getu (@haimanot_getu).</description>
    <link>https://dev.to/haimanot_getu</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F757610%2F7f82d2e4-e099-42b2-8bd9-e24c011f8dfd.jpg</url>
      <title>DEV Community: Haimanot Getu</title>
      <link>https://dev.to/haimanot_getu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/haimanot_getu"/>
    <language>en</language>
    <item>
      <title>I built a bootstrapped uptime and competitor intelligence SaaS for eCommerce</title>
      <dc:creator>Haimanot Getu</dc:creator>
      <pubDate>Mon, 08 Jun 2026 10:11:44 +0000</pubDate>
      <link>https://dev.to/haimanot_getu/i-built-a-bootstrapped-uptime-and-competitor-intelligence-saas-for-ecommerce-3e3k</link>
      <guid>https://dev.to/haimanot_getu/i-built-a-bootstrapped-uptime-and-competitor-intelligence-saas-for-ecommerce-3e3k</guid>
      <description>&lt;p&gt;I run a small side project called Beaconmon. It monitors websites for uptime, tracks competitor pricing and promotions, and sends intelligence digests to eCommerce store owners. Shopify and WooCommerce are the primary target.&lt;/p&gt;

&lt;p&gt;Here is how it is built and the decisions that shaped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most uptime tools are overkill for a small eCommerce store. A Shopify merchant does not need Datadog. They need to know when their store goes down, when a competitor drops prices before a sale weekend, and when a checkout page breaks.&lt;/p&gt;

&lt;p&gt;The interesting constraint: competitor tracking is not just a nice-to-have. It is the primary reason someone would pay for this over a free tier of Pingdom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Next.js 15 (App Router) for the dashboard and the marketing site, combined in one app&lt;/li&gt;
&lt;li&gt;  TypeScript everywhere in a pnpm monorepo&lt;/li&gt;
&lt;li&gt;  PostgreSQL + Prisma for data, with a separate workers process for background jobs&lt;/li&gt;
&lt;li&gt;  BullMQ + Redis for the job queue&lt;/li&gt;
&lt;li&gt;  better-auth for authentication (sessions, OAuth)&lt;/li&gt;
&lt;li&gt;  Freemius as the merchant of record (not Stripe, more on that below)&lt;/li&gt;
&lt;li&gt;  Resend + React Email for transactional and lifecycle emails&lt;/li&gt;
&lt;li&gt;  Sentry + PostHog for error tracking and analytics&lt;/li&gt;
&lt;li&gt;  Docker Compose for both local dev and production on a single VPS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monorepo has two apps and several packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
  web/       # Next.js: dashboard + marketing
  workers/   # Background job workers
packages/
  checker/   # Core check evaluation logic
  queue/     # BullMQ queue definitions
  db/        # Prisma schema + repositories
  types/     # Shared TypeScript types
  billing/   # Freemius integration
  ai/        # AI provider abstraction (noop by default)
  logger/    # Structured logging (Pino)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three architectural decisions worth talking about
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Two consecutive failures before marking a monitor down
&lt;/h3&gt;

&lt;p&gt;False positives are the biggest trust killer in uptime monitoring. A single timeout from a flaky CDN node will fire a 2am alert and destroy your credibility with the user.&lt;/p&gt;

&lt;p&gt;The evaluation logic in &lt;code&gt;packages/checker/&lt;/code&gt; requires two consecutive failures before transitioning a monitor to down. On the first failure it records the check result but holds the incident open. If the next check passes, it closes clean. If it fails again, it fires.&lt;/p&gt;

&lt;p&gt;This cut false positive alerts to near zero in testing against real Shopify stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Status pages read from Redis, not Postgres
&lt;/h3&gt;

&lt;p&gt;Status pages are ISR with a 30-second revalidate. When a store goes down, the merchant's customers hit the status page simultaneously. That is easily 100 to 300 requests per minute on a small VPS.&lt;/p&gt;

&lt;p&gt;If that read hits Postgres directly, the database becomes the bottleneck at exactly the worst moment. Instead, the workers write incident state to Redis on every check, and the status page reads from there. Postgres is never in the critical path during an incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Content monitoring with Cheerio, no Playwright
&lt;/h3&gt;

&lt;p&gt;The competitor tracking feature diffs HTML content over time to detect price changes and promotional banners. The obvious tool is a headless browser, which handles JavaScript-rendered content perfectly.&lt;/p&gt;

&lt;p&gt;I do not run Playwright in the worker fleet. The entire production setup runs on a single VPS. A Playwright worker pool would consume 2 to 4 GB of RAM and make the whole deployment fragile.&lt;/p&gt;

&lt;p&gt;Cheerio handles the vast majority of eCommerce content just fine, JavaScript-rendered content is explicitly out of scope, and RAM stays predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The billing choice: Freemius over Stripe
&lt;/h2&gt;

&lt;p&gt;Freemius is a merchant of record for software. They handle VAT, sales tax, and payment processing. For a bootstrapped solo project, not dealing with tax compliance in 50 US states and the EU is worth the higher fee.&lt;/p&gt;

&lt;p&gt;The tradeoff: Freemius has a clunkier API and their SDK is less polished than Stripe's. I wrapped it in a &lt;code&gt;packages/billing&lt;/code&gt; package so the rest of the codebase does not care which billing provider is underneath.&lt;/p&gt;

&lt;p&gt;One plan ID per tier, not two (monthly/annual). Freemius handles the billing cycle internally as an integer (&lt;code&gt;1 = monthly&lt;/code&gt;, &lt;code&gt;12 = annual&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The workers
&lt;/h2&gt;

&lt;p&gt;The workers app runs a set of specialized BullMQ workers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;check-worker&lt;/code&gt; runs HTTP checks on a schedule&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;content-worker&lt;/code&gt; diffs HTML snapshots for content monitoring&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;competitor-page-worker&lt;/code&gt; fetches competitor pages&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;alert-worker&lt;/code&gt; sends notifications through configured channels (email, Slack, Discord, SMS, webhook)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;digest-worker&lt;/code&gt; generates intelligence digests&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;scheduler&lt;/code&gt; enqueues jobs at the correct intervals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scheduler is pinned to one replica. BullMQ uses Redis for job storage with &lt;code&gt;noeviction&lt;/code&gt; policy and AOF durability. Job data must not be evicted.&lt;/p&gt;

&lt;p&gt;Alert sends are idempotent: before sending any notification, the worker checks for an existing send for the same &lt;code&gt;(incident_id, channel_id)&lt;/code&gt; within the last 5 minutes.&lt;/p&gt;

&lt;p&gt;This handles the case where a worker crashes mid-send and retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tenant isolation
&lt;/h2&gt;

&lt;p&gt;Every web-facing database function takes &lt;code&gt;teamId&lt;/code&gt; as its first argument.&lt;/p&gt;

&lt;p&gt;No exceptions.&lt;/p&gt;

&lt;p&gt;This is enforced by convention and code review, not by row-level security at the database layer. Worker repositories are exempt because job payloads are already tenant-scoped at enqueue time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;Beaconmon is live.&lt;/p&gt;

&lt;p&gt;The onboarding wizard detects Shopify and WooCommerce stores at step one and bulk-creates the competitor monitors at the end of the flow rather than incrementally.&lt;/p&gt;

&lt;p&gt;If you are curious about the codebase or have questions about any of these decisions, drop them in the comments.&lt;/p&gt;

&lt;p&gt;Built with Next.js, BullMQ, Prisma, and too much Redis.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>nextjs</category>
      <category>saas</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
