Vinod KumarVinod Kumar

Building DomainPulse: A Domain Expiry Tracking SaaS

·3 min read
gonext.jssaaspostgresqlsystem design

The Problem

Existing domain monitoring tools like ExpiredDomains.net are built for SEO professionals — packed with features that overwhelm anyone who just wants to watch a few domains. I wanted something simple: add a domain, get notified before it expires.

What DomainPulse Does

DomainPulse is a minimalist, mobile-first platform for tracking domain name expiration dates. The core workflow is straightforward:

  1. Look up any domain to see its WHOIS data instantly
  2. Add it to your watchlist
  3. Get email alerts at customizable intervals (90, 60, 30, 14, 7, and 1 day before expiry)

It also includes magic link authentication, Google/GitHub OAuth, and Stripe-powered billing with a free tier (3 domains) and a Pro tier (50 domains).

The Tech Stack

I chose Go for the backend because of its performance characteristics and straightforward concurrency model — ideal for handling WHOIS lookups and background job processing.

Backend

  • Go + Fiber — Express-like HTTP framework with excellent performance
  • PostgreSQL with sqlc — Type-safe SQL code generation, no ORM overhead
  • River — Go-native background job queue backed by Redis for scheduled WHOIS refreshes and alert delivery
  • Redis — Caching WHOIS responses and rate limiting
  • Resend — Transactional email delivery

Frontend

  • Next.js 14 with App Router and TypeScript
  • Tailwind CSS — Mobile-first responsive design
  • Custom auth context — JWT-based session management

Architecture Decisions

Why Go Over Node.js?

WHOIS lookups are I/O-bound operations that benefit from Go's goroutine model. The backend handles concurrent lookups efficiently without the callback complexity. The likexian/whois library provides pure-Go WHOIS resolution with no external dependencies.

Separation of API and Worker

The backend runs as two separate processes:

  • API server (cmd/api/) — Handles HTTP requests via Fiber
  • Worker (cmd/worker/) — Processes background jobs via River (WHOIS refreshes, alert emails)

This separation means the API stays responsive even during heavy background processing.

Type-Safe SQL with sqlc

Instead of an ORM, I used sqlc to generate Go code directly from SQL queries. You write the SQL, sqlc generates type-safe Go functions:

-- name: GetUserDomains :many
SELECT * FROM domains
WHERE user_id = $1
ORDER BY expiry_date ASC;

This gives you full control over queries while maintaining type safety — the best of both worlds.

WHOIS Caching

WHOIS lookups are slow (1-2 seconds) and rate-limited by registrars. DomainPulse uses a shared cache in PostgreSQL (whois_cache table) so multiple users watching the same domain don't trigger redundant lookups.

Domain Status Tracking

Each domain is tracked with a status that maps to visual indicators in the UI:

  • Registered — Currently owned, expiry date known
  • Expiring Soon — Within the alert window
  • Grace Period — Expired but still reclaimable by the owner
  • Available — Dropped and available for registration
  • Unknown — WHOIS data unavailable

What I Learned

Building DomainPulse reinforced a few things:

  • Start with the data model. Getting the PostgreSQL schema right early (users, domains, alerts, whois_cache) made everything downstream simpler.
  • Background jobs are essential for SaaS. Anything that touches external APIs (WHOIS, email, Stripe) should be async.
  • Mobile-first isn't just CSS. It changes how you think about information hierarchy and interaction patterns.
  • Go's standard library is powerful. Between net/http, encoding/json, and the ecosystem, you need surprisingly few dependencies.

What's Next

The roadmap includes a browser extension for one-click domain watching, bulk CSV import for Pro users, and an MCP server for AI agent integration. Stay tuned.