Skip to content

About

Open-source domain lifecycle monitoring platform for RDAP, DNS, and domain status tracking

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

ย 

History

57 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Domain Monitor

English | ็ฎ€ไฝ“ไธญๆ–‡

๐Ÿ›ฐ๏ธ Self-hosted domain monitoring โ€” RDAP, DNS, SSL & HTTP, all in one place.

CI License: MIT Release

Keep your domains under control: track registration data, spot DNS and SSL changes, catch HTTP failures โ€” and get a ping the moment something moves. ๐Ÿ””

  • โšก Event-driven notifications โ€” changes become events, events become notifications
  • ๐Ÿ“จ Telegram / Webhook / Email โ€” delivered straight into the tools you already use
  • ๐Ÿ“… Manual expiration & reminders โ€” set expiry dates by hand (RDAP or manual source), register a platform, and schedule expiration reminders
  • ๐Ÿšš Delivery Worker โ€” one-shot CLI, schedule it with cron, no daemon to babysit
  • ๐Ÿ” Admin authentication โ€” setup wizard, login, one-time recovery code
  • ๐ŸŒ English / ็ฎ€ไฝ“ไธญๆ–‡ UI

Dashboard

๐Ÿ“ธ Screenshots are from an earlier release; today's UI also has admin authentication and Telegram channels.

๐Ÿค” Why Domain-Monitor?

A domain is never just "up or down". The interesting stuff is what happens in between:

  • ๐Ÿงญ DNS changes โ€” records added, removed, or swapped (A / AAAA / CNAME / MX / NS / TXT / CAA)
  • ๐Ÿ”’ SSL changes โ€” certificates expiring, replaced, or mismatched with the hostname
  • ๐ŸŒ HTTP failures / recovery โ€” downtime, status changes, redirect drift
  • ๐Ÿชช Registration info โ€” registrar, expiry, nameservers, RDAP status

Domain Monitor turns those changes into events, matches them against your rules, and delivers them as notifications โ€” so you hear about it when something changes, not after it breaks. ๐ŸŽฏ

โšก Quick Start

git clone https://github.com/hxx0611/Domain-Monitor.git
cd Domain-Monitor
pnpm install
cp .env.example .env
pnpm dev

Then open http://localhost:3000 โ€” and you're in. ๐ŸŽ‰

What you need: Node.js 22 LTS or newer (22 LTS recommended; 24 / 26 are CI-tested) and pnpm. Works on Linux, macOS, and Windows โ€” better-sqlite3 ships prebuilt binaries, so a plain pnpm install needs no Python or C++ build tools. ๐Ÿ‘Œ

๐Ÿ‘‡ Two ways to run this project โ€” pick one and stick with it, don't mix the command sets:

  • ๐Ÿ–ฅ๏ธ Option A โ€” Local / Node (the quick start above): better-sqlite3 + SQLite, pnpm dev / pnpm build / pnpm start, database lives in a local file data/domain-monitor.db.
  • โ˜๏ธ Option B โ€” Cloudflare Production (the whole section below): Cloudflare Worker + OpenNext + D1, custom-worker.ts + scheduled(), database in cloud D1. If your goal is a Cloudflare deployment, keep scrolling โ€” and do NOT use the local commands above.

Cloudflare Production Deployment (Option B)

๐Ÿงญ This section is written for people who have never used Cloudflare Workers / D1 / OpenNext. Just follow the steps in order โ€” no skipping. By the end you'll have a fully self-owned Cloudflare deployment (your own Worker, your own D1, your own domain) โ€” you will not touch anyone else's production resources. ๐Ÿ™Œ

๐Ÿšจ๐Ÿšจ๐Ÿšจ READ THIS BEFORE YOU START (SAFETY WARNING) ๐Ÿšจ๐Ÿšจ๐Ÿšจ

  1. ๐Ÿšซ Never use the database_id from the repo's wrangler.prod.jsonc. It is the author's production database ID โ€” it doesn't exist in your account and deploying with it will fail; and if you ever run it against the author's account, it could touch the author's production data. You must create your own D1 and replace database_id with yours (see Step 3).
  2. โš ๏ธ If a Worker or D1 named domain-monitor already exists in your Cloudflare account (e.g. you deployed before), wrangler d1 create domain-monitor will conflict and wrangler deploy will overwrite your existing Worker of the same name. Use a unique name instead, e.g. domain-monitor-yourname, and keep the Worker name, the D1 name, the "name"/database_name in wrangler.prod.jsonc, and every command below consistent.
  3. ๐Ÿ”’ Never run wrangler secret put, wrangler deploy, or any other mutating command against a Worker you don't own. First confirm the current Cloudflare account is yours.

0. Prerequisites โœ…

Make sure you've got these ready:

  • ๐ŸŒฉ๏ธ A Cloudflare account (the free tier is enough)
  • ๐ŸŒ A domain (recommended but optional โ€” you can validate on a *.workers.dev temporary domain first)
  • ๐Ÿ“ฆ Node.js 22+ and pnpm 10.14+ (recommend 11; the repo lockfile is v9 and the allowBuilds syntax in pnpm-workspace.yaml requires pnpm โ‰ฅ10.14 โ€” pnpm 9 will fail to install)
  • ๐Ÿ’ป A machine with bash; Windows users see section 7 for the PowerShell build command

1. Clone and install ๐Ÿ› ๏ธ

git clone https://github.com/hxx0611/Domain-Monitor.git
cd Domain-Monitor
pnpm install

Wrangler (Cloudflare's official CLI) and the OpenNext Cloudflare adapter are not formal project dependencies, so grab them as dev dependencies:

pnpm add -D wrangler @opennextjs/cloudflare

โš ๏ธ pnpm 10.14+ (recommend 11) requires approving workerd's build script: wrangler depends on workerd, and pnpm blocks dependency postinstall scripts by default, so you may see ERR_PNPM_IGNORED_BUILDS: Ignored build scripts: workerd. If you don't approve it, the OpenNext build below will fail. If your pnpm is older than 10.14, upgrade first with corepack use pnpm@11 (or npm i -g pnpm@11) โ€” otherwise allowBuilds/approve-builds are not available.

Recommended (simplest, won't break the file): run

pnpm approve-builds

Select workerd when prompted, then re-run pnpm install. ๐Ÿ‘

Alternative (manual edit of pnpm-workspace.yaml):

  1. Open pnpm-workspace.yaml and check whether pnpm already inserted a placeholder line (e.g. workerd: set this to true or false);
  2. If a workerd entry already exists: change its value to workerd: true โ€” never add a second line;
  3. If no workerd entry exists: append one line workerd: true under allowBuilds: (match the existing indentation).

โš ๏ธ Do not add allowBuilds: or workerd: twice in the same YAML file โ€” duplicate keys make pnpm install fail with duplicated mapping key. Re-run pnpm install after editing and confirm the warning is gone.

Quick sanity check:

pnpm exec wrangler --version            # should print 4.x
pnpm exec opennextjs-cloudflare --help  # should print help

2. Create a Cloudflare API Token ๐Ÿ”‘

Head to the Cloudflare Dashboard โ†’ My Profile โ†’ API Tokens โ†’ Create Token, use the "Edit Cloudflare Workers" template (or create a custom token), and grant at least these permissions (all Edit):

Resource Permission Purpose
Account โ†’ Workers Scripts Edit Upload/update the Worker
Account โ†’ D1 Edit Create/manage the D1 database
Zone โ†’ Workers Routes (optional) Edit Bind a custom domain
Zone โ†’ Zone (optional) Read Read your domain zone

Then export the token as environment variables โ€” and remember: never put it into the repo, .env, wrangler.prod.jsonc, or any config file ๐Ÿšซ:

export CLOUDFLARE_API_TOKEN=your-token
export CLOUDFLARE_ACCOUNT_ID=your-account-id   # from the Dashboard footer

(Or use wrangler login browser auth instead โ€” pick one.)

3. Create your own D1 database โš ๏ธ most important

โš ๏ธ If a D1 or Worker named domain-monitor already exists in your account, use a unique name instead, e.g. domain-monitor-yourname (swap yourname for your own identifier). Never overwrite resources that are not yours. Use that unique name in every command below.

pnpm exec wrangler d1 create domain-monitor

You'll see output like this:

โœ… Successfully created DB 'domain-monitor' in region APAC
Created your new D1 database.
[[d1_databases]]
binding = "DB"
database_name = "domain-monitor"
database_id = "<YOUR_DATABASE_ID>"

โš ๏ธ Never use the database_id already present in the repo's wrangler.prod.jsonc โ€” it belongs to the author's production environment, it doesn't exist in your account, deploying with it will fail (or worse: it could touch the author's production data if you ever run it against that account). Copy the <YOUR_DATABASE_ID> from the output above.

Now open wrangler.prod.jsonc and replace d1_databases[0].database_id with your own <YOUR_DATABASE_ID> (keep binding = "DB"). If you used a unique name (e.g. domain-monitor-yourname), also change d1_databases[0].database_name and the top-level "name" to that same name โ€” the Worker name, D1 name, config file, and every command below must all be consistent. โœ…

4. Apply D1 migrations ๐Ÿ“œ

Run this from the repo root (after cd Domain-Monitor) and always pass --config wrangler.prod.jsonc โ€” there is no default wrangler.jsonc at the repo root, so omitting --config fails with No configuration file found.

pnpm exec wrangler d1 migrations apply domain-monitor --remote --config wrangler.prod.jsonc
  • This applies src/db/migrations/ to the D1 database you just created (--remote = cloud).
  • You should see 0000โ€“0007 all applied.
  • โš ๏ธ pnpm db:migrate is a local SQLite migration and does NOT replace this step โ€” it never touches Cloudflare D1.

5. Set Secrets (ENCRYPTION_KEY / SESSION_SECRET) ๐Ÿ”

Two secrets to set up:

  • ENCRYPTION_KEY: encrypts sensitive data such as Telegram tokens (AES-256-GCM). โš ๏ธ Once set, losing it makes the encrypted data unrecoverable.
  • SESSION_SECRET: signs login sessions (cookies).

Run from the repo root and always pass --config wrangler.prod.jsonc so the secrets go into your Worker (the one you named in Step 3).

openssl rand -hex 32 | pnpm exec wrangler secret put ENCRYPTION_KEY --config wrangler.prod.jsonc
openssl rand -hex 32 | pnpm exec wrangler secret put SESSION_SECRET --config wrangler.prod.jsonc

On Windows PowerShell, if openssl isn't available, generate the 64-hex value first and paste it when prompted:

-join ([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32) | ForEach-Object { $_.ToString("x2") })

Each command prompts for the secret โ€” just paste the 64-hex value. These values are never written to git, README, .env, or any config file; they live only in Cloudflare Secrets. ๐Ÿ—๏ธ

โš ๏ธ Never run wrangler secret put against a Worker you don't own โ€” it writes the secret to that Worker and overwrites any existing value. Before running, confirm the "name" in wrangler.prod.jsonc is your own Worker name.

6. Clean the old build cache ๐Ÿงน

Before every Cloudflare build (the reason is in section 8):

rm -rf .next .open-next

Windows PowerShell:

Remove-Item -Recurse -Force .next,.open-next

7. Build OpenNext (Cloudflare) artifacts ๐Ÿ—๏ธ

OPENNEXT_CLOUDFLARE=1 SKIP_WRANGLER_CONFIG_CHECK=yes pnpm exec opennextjs-cloudflare build

Windows PowerShell (same build command, environment variables set via $env:):

$env:OPENNEXT_CLOUDFLARE="1"
$env:SKIP_WRANGLER_CONFIG_CHECK="yes"
pnpm exec opennextjs-cloudflare build
Remove-Item Env:OPENNEXT_CLOUDFLARE,Env:SKIP_WRANGLER_CONFIG_CHECK

What's actually happening here:

  • OPENNEXT_CLOUDFLARE=1 is not optional ๐Ÿšซ: it makes Next.js use tsconfig.cf.json's stub aliases (redirecting @/db to the Cloudflare stub) and makes webpack redirect @/db / @/db/node-singleton to the stub, so better-sqlite3 and other Node/SQLite dependencies never enter the Cloudflare Worker bundle. Without it, the build mixes in Node/SQLite runtime code and the deployed Worker fails at runtime.
  • SKIP_WRANGLER_CONFIG_CHECK=yes: the repo root only has wrangler.prod.jsonc (OpenNext only looks for wrangler.jsonc/wrangler.toml at the root by default), so we skip OpenNext's own config-existence check. This does not affect wrangler deploy (deploy explicitly uses --config wrangler.prod.jsonc).
  • Success looks like: .open-next/worker.js and .open-next/assets/ (with BUILD_ID) exist. โœ…

Why clean the cache first: if you previously ran a plain pnpm build without OPENNEXT_CLOUDFLARE=1, the .next cache may contain Node/SQLite-path build results, and reusing it pollutes the Cloudflare artifacts. Deleting .next .open-next before building is a cheap, reliable safeguard.

8. dry-run / bundle safety verification (recommended) ๐Ÿ”

pnpm exec wrangler deploy --config wrangler.prod.jsonc --dry-run
  • Success: the output shows Read N files from the assets directory .open-next/assets, env.DB / env.ASSETS bindings, and --dry-run: exiting now.
  • Extra mile (optional): export the final bundle and confirm there's no SQLite runtime in it:
pnpm exec wrangler deploy --config wrangler.prod.jsonc --dry-run --outdir /tmp/dm-bundle
grep -c "new Database(" /tmp/dm-bundle/custom-worker.js   # expect 0
grep -c "better-sqlite3" /tmp/dm-bundle/custom-worker.js  # only allowed inside drizzle-orm package path strings

Cloudflare deployments use D1, not better-sqlite3/SQLite. The bundle must not contain runtime references to new Database( / DATABASE_URL / node:sqlite. ๐Ÿšซ

9. Deploy the Worker ๐Ÿš€

pnpm exec wrangler deploy --config wrangler.prod.jsonc
  • Order matters: build first (section 7) so .open-next/assets exists; otherwise assets.directory .open-next/assets does not exist fails immediately.
  • You should see Uploaded domain-monitor / Deployed domain-monitor plus a version. ๐ŸŽ‰
  • โš ๏ธ Final check before deploying: the top-level "name" in wrangler.prod.jsonc must be your own Worker name (default domain-monitor, or domain-monitor-yourname if you used a unique name). wrangler deploy uploads/overwrites the Worker with that name โ€” never overwrite someone else's Worker of the same name.

10. Cron scheduling โฐ

wrangler.prod.jsonc already includes:

"triggers": { "crons": ["0 * * * *"] }   // every hour on the hour
  • After deployment, Cron is managed automatically by Cloudflare โ€” no Linux cron needed. ๐Ÿ˜Œ
  • The Cloudflare production entry point is the Worker's scheduled() (which calls runOnce via D1); this is not the local pnpm worker.

11. Bind a domain ๐ŸŒ

  • Verify first: https://domain-monitor.<your-workers-subdomain>.workers.dev should load (if you used a unique name, use https://domain-monitor-yourname.<your-workers-subdomain>.workers.dev).

  • Then bind a custom domain (optional, recommended): Dashboard โ†’ Workers & Pages โ†’ your Worker โ†’ Settings โ†’ Domains & Routes โ†’ Add โ†’ Custom Domain, enter monitor.<your-domain> and confirm the automatic DNS setup.

  • โš ๏ธ Do not copy the author's domain (e.g. monitor.snooze.eu.cc) โ€” use your own. ๐Ÿšซ

12. First-visit initialization (/setup) ๐Ÿ‘‹

Open https://monitor.<your-domain> (or the workers.dev URL):

  1. If unconfigured, you're redirected to /setup โ†’ create the admin account
  2. ๐Ÿ’พ Save the recovery code โ€” it's the only credential to reset your password, and it's shown once
  3. Log in
  4. ๐Ÿ“จ Notifications โ†’ add a Telegram channel: paste the Bot Token; the system verifies it via Telegram getMe, then stores it encrypted with AES-256-GCM
  5. ๐Ÿ”’ The Telegram Bot Token does not go into .env โ€” it lives only in your D1 database (encrypted)

13. Deployment success Checklist โœ…

  • Git clone completed
  • pnpm install completed
  • pnpm exec wrangler --version works
  • pnpm exec opennextjs-cloudflare --help works
  • Your own D1 created
  • wrangler.prod.jsonc database_id is your <YOUR_DATABASE_ID>
  • wrangler d1 migrations apply --remote --config wrangler.prod.jsonc succeeded (0000โ€“0007)
  • ENCRYPTION_KEY configured (wrangler secret put)
  • SESSION_SECRET configured (wrangler secret put)
  • .next / .open-next cleaned
  • OPENNEXT_CLOUDFLARE=1 build succeeded
  • .open-next/worker.js exists
  • .open-next/assets/ exists
  • wrangler deploy --dry-run succeeded
  • Worker deployed
  • Cron configured (0 * * * *, Cloudflare-managed)
  • Custom domain reachable (or workers.dev reachable)
  • /setup reachable
  • Admin created
  • Recovery code saved
  • Telegram channel verified

๐ŸŽ‰ All boxes ticked? Your deployment is done. Well played.


Back to top

โœจ Features

๐Ÿง  Domain Intelligence

  • Manage all monitored domains in one place โ€” self-hosted local storage (SQLite)
  • Automatic RDAP lookup on creation: registrar, expiry, nameservers, RDAP status (IANA bootstrap, 590+ TLDs)
  • ๐Ÿ”Ž Ownership-aware RDAP fallback: when a subdomain has no independent RDAP object, the query falls back to the registered domain and reports ownership = parent โ€” the parent's expiration/registrar/nameservers are never stored on the subdomain's own fields, and the UI shows Unavailable for the subdomain's registration info
  • โœ‹ Manual expiration (source = manual): set registration date, expiration date, registration platform and management URL by hand โ€” perfect for domains whose RDAP data is unreliable, missing, or simply not what you want to display. Manual dates are never overwritten by RDAP refreshes (the refresh only updates RDAP metadata, or clears it for no-object / parent results)
  • โณ Expiration reminders: per-domain reminder rules (e.g. 30 days before expiry); the notification worker evaluates them and emits expiration_reminder events (see Delivery Worker below for the current availability note)
  • ๐Ÿงน Domain normalization and validation (accepts https://example.com/path, stores example.com)
  • ๐Ÿ”„ Manual RDAP refresh anytime

๐Ÿงญ DNS Monitoring

  • DNS-over-HTTPS based monitoring (Cloudflare DoH, resolver swappable via DNS_DOH_ENDPOINT)
  • Tracks A / AAAA / CNAME / MX / NS / TXT / CAA records
  • Historical snapshots with added / removed record detection (TTL-only changes ignored)
  • ๐Ÿ›ก๏ธ Atomic failed-check handling โ€” a partial failure never deletes old data

๐Ÿ”’ SSL Monitoring

  • TLS certificate inspection (Node.js native TLS)
  • Expiration tracking: valid / expires soon / expired
  • Hostname mismatch detection (SAN vs queried domain)
  • Certificate fingerprint / replacement detection, TLS version and cipher information

๐ŸŒ HTTP Monitoring

  • HTTP status classification and response-time tracking
  • Redirect tracking (count and final URL)
  • Connection-failure detection (down)
  • Per-check history

๐Ÿ”” Notifications

  • Domain lifecycle events from DNS / SSL / HTTP checks
  • Channels: Telegram, Email API, and Webhook
  • ๐Ÿงช Test notifications (v0.8.4): admin-triggered Send Test Notification on each Telegram channel โ€” exactly one event + one delivery + one message through the existing factory/sender/encrypted-secret pipeline, explicitly labelled Test Notification, never routed through rules or expiry logic
  • ๐ŸŒ Channel-level notification language (v0.8.6): per-channel language (en / zh-CN) selected in the channel edit form; message template and event labels are localized, machine state values stay canonical
  • ๐Ÿ• Channel-level notification timezone (v0.8.7): per-channel IANA timezone (default UTC) selected in the channel edit form; the Telegram message timestamp renders as YYYY-MM-DD HH:mm:ss (Timezone) via Intl.DateTimeFormat (DST-aware), internal storage stays UTC
  • Rule-based delivery matching (global or per-domain rules, by source / event type โ€” including the expiration_reminder event type)
  • ๐ŸŽ›๏ธ Notification configuration UI โ€” channel CRUD (create / edit / toggle / delete), rule CRUD
  • ๐Ÿ” Telegram bot tokens validated via getMe (server-side) and stored AES-256-GCM encrypted (ENCRYPTION_KEY), with legacy TELEGRAM_BOT_TOKEN env fallback
  • ๐Ÿ“ฌ Delivery history with status tracking (pending / sending / sent / failed) and manual retry

๐Ÿง‘โ€๐Ÿ’ผ Admin Authentication

  • One-time setup wizard (/setup) โ€” creates the admin password (scrypt-hashed) and a one-time recovery code
  • HMAC-signed session cookie โ€” login / logout, protected pages and Server Actions
  • ๐Ÿ”„ Password recovery rotates the session secret, invalidating all old sessions

๐Ÿšš Delivery Worker

Availability note (v0.8.3): the worker is enabled in production โ€” the hourly watchdog (scripts/worker-watchdog.sh) runs as a single instance and ticks every hour (tsx --conditions=react-server scripts/worker.ts --limit 50). Expiration-reminder evaluation and delivery are live; real notification delivery is exercised only through explicitly approved safety gates (no real Telegram/Webhook/Email sends have been performed as part of this release).

  • One-shot CLI (pnpm worker) โ€” schedule with cron or the bundled watchdog, no daemon, no HTTP endpoint
  • โš™๏ธ Automatic Event โ†’ Delivery generation inside the check transaction (atomic)
  • โณ Expiration reminders: evaluateExpirationReminders() runs inside the worker tick and emits expiration_reminder events (source expiration) for domains whose reminder day has arrived, deduplicated so a domain is reminded once per day
  • ๐Ÿ”— Event โ†’ Delivery together: insertEventsAndGenerateDeliveries creates the event and its deliveries in one step; concurrent ticks are safe (SQLite CAS) โ€” at most one event, one delivery and one sender invocation per reminder per day
  • ๐Ÿ›ก๏ธ Stale sending recovery (crash-safe) and concurrent-worker safety (SQLite CAS)

๐ŸŒ Bilingual UI

  • English / ็ฎ€ไฝ“ไธญๆ–‡ language switching in the header
  • Locale-aware UI dictionary; preference stored in the domain-monitor-locale cookie (en / zh-CN, default en)
  • Cookie + Server Action + router.refresh() โ€” no URL prefix, no middleware, no third-party i18n dependency
  • ๐Ÿค– Machine values (delivery status, event types, sources) are never translated

โš™๏ธ How It Works

flowchart LR
    A[Domain Check] --> B[Event]
    B --> C[Rule Matching]
    C --> D[Delivery Queue]
    D --> E[Worker / Cron]
    E --> F[Telegram / Webhook / Email]
Loading

A check writes its snapshot, its events, and the matching pending deliveries in one transaction. The delivery worker claims pending deliveries (atomic CAS) and calls the senders. Retrying a failed delivery from the UI works end-to-end. ๐Ÿ’ช

Domain details โ€” RDAP, DNS changes, SSL certificates, HTTP status

๐Ÿ›ก๏ธ Security by Design

  • ๐Ÿ” Admin authentication โ€” protected pages and Server Actions; scrypt password hashing; signed session cookies; recovery-code rotation invalidates old sessions
  • ๐Ÿ—๏ธ Encrypted secret storage โ€” Telegram bot tokens are stored AES-256-GCM encrypted (iv:tag:ciphertext, keyed by ENCRYPTION_KEY); tokens are never rendered back into HTML/RSC/client bundles โ€” only CONFIGURED/NOT CONFIGURED status is exposed
  • ๐Ÿšซ SSRF protection โ€” outbound requests are HTTPS-only, with per-redirect re-validation
  • ๐Ÿ”’ HTTPS-only outbound traffic, redirect re-validation on every hop
  • ๐Ÿ™ˆ Secret isolation โ€” API keys / webhook secrets / bot tokens never appear in the UI, worker output, or client bundle
  • โž• at-least-once delivery with stable eventId + deliveryId for receiver-side deduplication

๐Ÿ’ช Built for Reliability

  • ๐Ÿงช 849 tests covering services, state machines, senders, the delivery worker, manual expiration & reminders, worker runtime fixes (barrel import + delivery generation), the i18n core, admin authentication, domain/DNS action coverage, and the production backup mechanism
  • ๐Ÿงช 780 tests (v0.8.4) adds the controlled test-notification action contract (authorization, channel validation, dedup, single-send limits, leakage) and its real-DB integration path (encrypted-secret chain, sender success/failure, no domain/rule mutation)
  • ๐Ÿงช 813 tests (v0.8.7) adds notification timezone IANA validation + Intl rendering
  • ๐Ÿงช 849 tests (v0.8.8) adds domain/DNS action coverage (Phase 13B, 36 tests)
  • ๐Ÿ›ก๏ธ SSRF-guarded webhook and email senders
  • ๐Ÿ”’ SQLite concurrency tested โ€” atomic claim (CAS) + busy_timeout = 5000
  • ๐Ÿ  Self-hosted โ€” your data stays on your machine

๐Ÿ“Š Current Status

Current release: v0.8.9 โ€” Documentation & Operations Closeout (v0.8.8 โ€” Domain/DNS Action Coverage, Notification Timezone, Windows CI Fix)

Supported today:

  • โœ… Domain management
  • โœ… RDAP information
  • โœ‹ Manual expiration โ€” set registration / expiration dates, registration platform and management URL manually; manual dates survive RDAP refreshes
  • โณ Expiration reminders โ€” per-domain reminder days, evaluated by the worker as expiration_reminder events
  • โœ… DNS monitoring
  • โœ… SSL certificate monitoring
  • โœ… HTTP health checks
  • โœ… Notification system (telegram / email / webhook channels, rules โ€” including expiration_reminder โ€” delivery history, manual retry)
  • โœ… Notification configuration UI (channel & rule CRUD, Telegram token setup with getMe verification)
  • ๐Ÿšš Delivery worker (automatic Event โ†’ Delivery โ†’ Send pipeline; enabled in production via the hourly watchdog โ€” see the availability note above)
  • ๐Ÿ” Admin authentication (setup wizard, login/logout, recovery code, protected pages)
  • ๐Ÿ—๏ธ Encrypted secret storage (AES-256-GCM, ENCRYPTION_KEY, legacy env fallback)
  • ๐ŸŒ Bilingual UI (English / ็ฎ€ไฝ“ไธญๆ–‡, cookie-based locale switching)

๐Ÿ“Œ DNS, SSL and HTTP checks are currently manual; automatic scheduling is planned for a future release.

The notification pipeline is fully closed: a check writes its snapshot, its events, and the matching pending deliveries in ONE transaction; the delivery worker consumes those pending deliveries and calls the senders. Retrying a failed delivery from the UI works end-to-end.

๐Ÿšš Delivery Worker (V0.7)

Notifications โ€” channels, rules, delivery history with Retry

The delivery worker is a one-shot CLI process that consumes the pending deliveries the notification pipeline records. It's the recommended way to run notifications on a self-hosted deployment. ๐Ÿ‘‡

โ–ถ๏ธ Run it

pnpm worker             # one tick, up to 50 pending deliveries
pnpm worker --limit 10  # cap this tick at 10 deliveries
pnpm worker --limit=10  # same

The worker runs one tick and exits โ€” it never stays resident, starts no interval, opens no HTTP endpoint, and keeps no background timers. It prints a single JSON summary line to stdout (exit 0), or a clear error to stderr (exit 1 for bad arguments or an uncaught error).

Summary shape (stable):

{ "expirationEvents": 0, "recovered": 0, "attempted": 0, "sent": 0, "failed": 0, "skipped": 0 }
  • expirationEvents โ€” expiration-reminder events emitted this tick (V0.8.3)
  • recovered โ€” stale sending deliveries moved back to pending (crash recovery)
  • attempted โ€” deliveries this tick tried to deliver
  • sent / failed / skipped โ€” outcomes (skipped = a concurrent worker claimed it first)

๐Ÿ”’ The worker never prints secrets: no API keys, no Authorization/Bearer values, no channel config JSON, no endpoint query strings.

โฐ Scheduling with cron

Recommended: the CLI worker plus an external scheduler (system cron or equivalent). Example crontab entry โ€” adjust the path for your deployment:

* * * * * cd /path/to/Domain-Monitor && pnpm worker >> /var/log/domain-monitor-worker.log 2>&1
  • Runs every minute; each run is a fresh one-shot process.
  • An empty queue exits immediately.
  • Default cap: 50 pending deliveries per tick.
  • ๐Ÿšซ No public HTTP endpoint is added โ€” scheduling stays fully external (no webhook scheduler endpoint, no serverless cron).
  • ๐Ÿ”’ Overlapping cron instances are safe: SQLite CAS guarantees only one worker ever claims a given delivery. Keep it at once per minute to avoid extra DB contention.

๐Ÿง  Runtime semantics

  • Check โ†’ Event โ€” recorded by the V0.6 pipeline (per-check transaction, deduplicated).
  • Event โ†’ Delivery โ€” automatic since V0.7: the check transaction creates the matching pending deliveries for every newly-recorded event (rule-matched, channel-deduplicated). Duplicate events (same dedup key) never re-generate deliveries.
  • Delivery โ†’ Send โ€” the worker claims pending deliveries (atomic CAS) and calls the existing senders.
  • โŒ failed โ€” the worker does not auto-retry failed deliveries; no backoff, no max-attempts.
  • ๐Ÿ” retry โ€” explicit only: retryDelivery() / the notification UI.
  • โฑ๏ธ stale sending โ€” the worker runs recoverStaleSending() at the start of every tick; the default stale threshold is 5 minutes.
  • โž• at-least-once โ€” a crash mid-send leaves sending, which the next tick recovers and sends again, so a delivery can be sent more than once. Receivers must deduplicate using the stable eventId + deliveryId in the payload. This is at-least-once, not exactly-once.
  • ๐Ÿ“œ historical events โ€” V0.7 does NOT backfill deliveries for events recorded before the upgrade; the worker only consumes the current pending queue.
  • SQLite busy_timeout = 5000 is enabled so the worker and the web app can write concurrently without immediate SQLITE_BUSY failures.
  • No daemon, no automatic retry, no backoff, no max-attempts, no distributed queue (Redis/Kafka), no HTTP scheduler endpoint, no serverless scheduler, no SLA/uptime monitoring.

๐Ÿงช Testing

pnpm test

Current test suite: 849 tests (57 files), covering domain validation (including manual expiration fields and reminder-day normalization), RDAP parsing and fallback ownership semantics, registration-platform validation, DNS normalization and diffing, SSL certificate parsing and diffing, HTTP status classification and SSRF-guarded fetching, the DNS/SSL/HTTP services, the notification event/rule/delivery state machine (including the expiration_reminder event type), SSRF-guarded webhook and email senders, automatic Event โ†’ Delivery generation, expiration-reminder evaluation, the delivery worker (including concurrent-tick dedup / CAS E2E), the controlled test-notification action (authorization, channel validation, dedup, single-send limits, secret leakage), admin authentication (sessions, setup/login/recovery), encrypted secret storage, Telegram sender secret resolution, the locale-aware i18n core (dictionaries, cookie fallback, client/server boundary), notification timezone (IANA validation and Intl rendering), the data repositories, and the domain/DNS action layer (create/update/refreshRdap/delete + admin guards).

Also run before pushing changes:

pnpm lint
pnpm format:check
pnpm build

๐Ÿ—๏ธ Architecture

UI (Next.js App Router)
        โ†“
Server Actions
        โ†“
Domain / RDAP / DNS services
        โ†“
Repository
        โ†“
SQLite
  • Next.js App Router + Server Actions
  • Drizzle ORM with SQLite (migrations in src/db/migrations/)
  • Cloudflare DoH for DNS queries (resolver swappable via DNS_DOH_ENDPOINT)
  • IANA RDAP bootstrap for registration data

See docs/development.md for detailed development notes. ๐Ÿ“š

๐Ÿ—„๏ธ Database

SQLite via Drizzle ORM (local/Node path โ€” Option A). Manage the local schema with the built-in commands:

pnpm db:generate   # Generate migration files
pnpm db:migrate    # Run migrations (local SQLite only)
pnpm db:studio     # Open the visual database browser

โš ๏ธ pnpm db:migrate only touches the local SQLite file (data/domain-monitor.db) and has nothing to do with Cloudflare D1. For a production deployment (Option B) the database is Cloudflare D1, and the migration command is:

pnpm exec wrangler d1 migrations apply domain-monitor --remote --config wrangler.prod.jsonc

Both paths share the migration files in src/db/migrations/, but do not use pnpm db:migrate to migrate D1. Full flow: see Cloudflare Production Deployment (Option B).

๐Ÿ—บ๏ธ Roadmap

  • V0.1 โ€” Domain management
  • V0.2 โ€” RDAP / WHOIS integration
  • V0.3 โ€” DNS monitoring
  • V0.4 โ€” SSL certificate monitoring
  • V0.5 โ€” HTTP health checks
  • V0.6 โ€” Notification system
  • V0.7 โ€” Notification delivery worker
  • V0.7.1 โ€” Bilingual UI
  • V0.7.3 โ€” Monitoring error clarity
  • V0.8.0 โ€” Admin authentication, Telegram notifications & encrypted secrets
  • V0.8.1 โ€” RDAP ownership & expiration fixes (bugfix release)
  • V0.8.2 โ€” Manual expiration, registration platform & expiration reminders (worker delivery enabled in production in v0.8.3)
  • V0.8.3 โ€” Production Worker enablement (hourly watchdog, expiration reminder delivery pipeline, worker runtime fixes) + migration journal repair for 0007
  • V0.8.4 โ€” Controlled test-notification action (sendTestNotificationAction, authorized, deduped, single-send limits, leakage-safe)
  • V0.8.5 โ€” Notification timezone (channel-level IANA timezone, Intl.DateTimeFormat rendering, zero migration)
  • V0.8.6 โ€” Bugfix release
  • V0.8.7 โ€” Notification timezone polish (validated channel timezone field in UI, 800 โ†’ 813 tests)
  • V0.8.8 โ€” Windows CI temp-DB deletion fix (closeDb() helper) + domain/DNS action test coverage (Phase 13B, 849 tests)
  • Operations (2026-08-20) โ€” Production backup via SQLite online backup API (Phase 13C), daily QwenPaw cron domain-monitor-daily-backup (13:00 Asia/Shanghai, 7-day retention, NFS persistent storage, failure โ†’ Telegram alert)
  • Audit (2026-08-20) โ€” Phase 13A security/reliability audit (PASS), Phase 13D SQLiteโ†’NFS migration preflight (blocked: current NFSv3 nolock mount is not suitable for a SQLite primary DB)
  • V0.8.9 โ€” Documentation & Operations Closeout (Phase 13A audit report archived; operations/disaster-recovery/handover docs updated; backup strategy + SQLite/NFS restriction documented)

๐Ÿ’พ Production persistence (current)

  • ๐Ÿ“ Production SQLite remains on local /tmp overlay: /tmp/domain-monitor/data/domain-monitor.db.
  • โ˜๏ธ Production backups are stored on NFS persistent storage (daily, 7-day retention).
  • โš ๏ธ WARNING: the current NFSv3 + nolock mount is NOT approved for hosting the production SQLite database (locking / fsync / hard-mount semantics). PostgreSQL or an appropriate persistent local volume remains a future architecture option โ€” not implemented.

๐Ÿค Contributing

Contributions are welcome! ๐ŸŽ‰

pnpm install
pnpm test
pnpm lint
pnpm build

See CONTRIBUTING.md for the full contribution guide.

๐Ÿ“„ License

MIT

About

Open-source domain lifecycle monitoring platform for RDAP, DNS, and domain status tracking

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages