๐ฐ๏ธ Self-hosted domain monitoring โ RDAP, DNS, SSL & HTTP, all in one place.
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
๐ธ Screenshots are from an earlier release; today's UI also has admin authentication and Telegram channels.
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. ๐ฏ
git clone https://github.com/hxx0611/Domain-Monitor.git
cd Domain-Monitor
pnpm install
cp .env.example .env
pnpm devThen 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 filedata/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.
๐งญ 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) ๐จ๐จ๐จ
- ๐ซ Never use the
database_idfrom the repo'swrangler.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 replacedatabase_idwith yours (see Step 3).โ ๏ธ If a Worker or D1 nameddomain-monitoralready exists in your Cloudflare account (e.g. you deployed before),wrangler d1 create domain-monitorwill conflict andwrangler deploywill 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_nameinwrangler.prod.jsonc, and every command below consistent.- ๐ 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.
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.devtemporary domain first) - ๐ฆ Node.js 22+ and pnpm 10.14+ (recommend 11; the repo lockfile is v9 and the
allowBuildssyntax inpnpm-workspace.yamlrequires pnpm โฅ10.14 โ pnpm 9 will fail to install) - ๐ป A machine with
bash; Windows users see section 7 for the PowerShell build command
git clone https://github.com/hxx0611/Domain-Monitor.git
cd Domain-Monitor
pnpm installWrangler (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 onworkerd, and pnpm blocks dependency postinstall scripts by default, so you may seeERR_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 withcorepack use pnpm@11(ornpm i -g pnpm@11) โ otherwiseallowBuilds/approve-buildsare not available.Recommended (simplest, won't break the file): run
pnpm approve-buildsSelect
workerdwhen prompted, then re-runpnpm install. ๐Alternative (manual edit of
pnpm-workspace.yaml):
- Open
pnpm-workspace.yamland check whether pnpm already inserted a placeholder line (e.g.workerd: set this to true or false);- If a
workerdentry already exists: change its value toworkerd: trueโ never add a second line;- If no
workerdentry exists: append one lineworkerd: trueunderallowBuilds:(match the existing indentation).
โ ๏ธ Do not addallowBuilds:orworkerd:twice in the same YAML file โ duplicate keys makepnpm installfail withduplicated mapping key. Re-runpnpm installafter 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 helpHead 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.)
โ ๏ธ If a D1 or Worker nameddomain-monitoralready exists in your account, use a unique name instead, e.g.domain-monitor-yourname(swapyournamefor your own identifier). Never overwrite resources that are not yours. Use that unique name in every command below.
pnpm exec wrangler d1 create domain-monitorYou'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 thedatabase_idalready present in the repo'swrangler.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. โ
Run this from the repo root (after
cd Domain-Monitor) and always pass--config wrangler.prod.jsoncโ there is no defaultwrangler.jsoncat the repo root, so omitting--configfails withNo 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:migrateis a local SQLite migration and does NOT replace this step โ it never touches Cloudflare D1.
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.jsoncso 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.jsoncOn 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 runwrangler secret putagainst a Worker you don't own โ it writes the secret to that Worker and overwrites any existing value. Before running, confirm the"name"inwrangler.prod.jsoncis your own Worker name.
Before every Cloudflare build (the reason is in section 8):
rm -rf .next .open-nextWindows PowerShell:
Remove-Item -Recurse -Force .next,.open-nextOPENNEXT_CLOUDFLARE=1 SKIP_WRANGLER_CONFIG_CHECK=yes pnpm exec opennextjs-cloudflare buildWindows 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_CHECKWhat's actually happening here:
OPENNEXT_CLOUDFLARE=1is not optional ๐ซ: it makes Next.js usetsconfig.cf.json's stub aliases (redirecting@/dbto the Cloudflare stub) and makes webpack redirect@/db/@/db/node-singletonto 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 haswrangler.prod.jsonc(OpenNext only looks forwrangler.jsonc/wrangler.tomlat the root by default), so we skip OpenNext's own config-existence check. This does not affectwrangler deploy(deploy explicitly uses--config wrangler.prod.jsonc).- Success looks like:
.open-next/worker.jsand.open-next/assets/(withBUILD_ID) exist. โ
Why clean the cache first: if you previously ran a plain
pnpm buildwithoutOPENNEXT_CLOUDFLARE=1, the.nextcache may contain Node/SQLite-path build results, and reusing it pollutes the Cloudflare artifacts. Deleting.next .open-nextbefore building is a cheap, reliable safeguard.
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.ASSETSbindings, 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 stringsCloudflare deployments use D1, not better-sqlite3/SQLite. The bundle must not contain runtime references to
new Database(/DATABASE_URL/node:sqlite. ๐ซ
pnpm exec wrangler deploy --config wrangler.prod.jsonc- Order matters: build first (section 7) so
.open-next/assetsexists; otherwiseassets.directory .open-next/assets does not existfails immediately. - You should see
Uploaded domain-monitor/Deployed domain-monitorplus a version. ๐ โ ๏ธ Final check before deploying: the top-level"name"inwrangler.prod.jsoncmust be your own Worker name (defaultdomain-monitor, ordomain-monitor-yournameif you used a unique name).wrangler deployuploads/overwrites the Worker with that name โ never overwrite someone else's Worker of the same name.
wrangler.prod.jsonc already includes:
- After deployment, Cron is managed automatically by Cloudflare โ no Linux cron needed. ๐
- The Cloudflare production entry point is the Worker's
scheduled()(which callsrunOncevia D1); this is not the localpnpm worker.
-
Verify first:
https://domain-monitor.<your-workers-subdomain>.workers.devshould load (if you used a unique name, usehttps://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. ๐ซ
Open https://monitor.<your-domain> (or the workers.dev URL):
- If unconfigured, you're redirected to
/setupโ create the admin account - ๐พ Save the recovery code โ it's the only credential to reset your password, and it's shown once
- Log in
- ๐จ Notifications โ add a Telegram channel: paste the Bot Token; the system verifies it via Telegram
getMe, then stores it encrypted with AES-256-GCM - ๐ The Telegram Bot Token does not go into
.envโ it lives only in your D1 database (encrypted)
- Git clone completed
-
pnpm installcompleted -
pnpm exec wrangler --versionworks -
pnpm exec opennextjs-cloudflare --helpworks - Your own D1 created
-
wrangler.prod.jsoncdatabase_idis your<YOUR_DATABASE_ID> -
wrangler d1 migrations apply --remote --config wrangler.prod.jsoncsucceeded (0000โ0007) -
ENCRYPTION_KEYconfigured (wrangler secret put) -
SESSION_SECRETconfigured (wrangler secret put) -
.next/.open-nextcleaned -
OPENNEXT_CLOUDFLARE=1build succeeded -
.open-next/worker.jsexists -
.open-next/assets/exists -
wrangler deploy --dry-runsucceeded - Worker deployed
- Cron configured (
0 * * * *, Cloudflare-managed) - Custom domain reachable (or workers.dev reachable)
-
/setupreachable - Admin created
- Recovery code saved
- Telegram channel verified
๐ All boxes ticked? Your deployment is done. Well played.
- 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 showsUnavailablefor 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 forno-object/ parent results) - โณ Expiration reminders: per-domain reminder rules (e.g. 30 days before expiry); the notification worker evaluates them and emits
expiration_reminderevents (see Delivery Worker below for the current availability note) - ๐งน Domain normalization and validation (accepts
https://example.com/path, storesexample.com) - ๐ Manual RDAP refresh anytime
- 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
- 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 status classification and response-time tracking
- Redirect tracking (count and final URL)
- Connection-failure detection (down)
- Per-check history
- Domain lifecycle events from DNS / SSL / HTTP checks
- Channels: Telegram, Email API, and Webhook
- ๐งช Test notifications (v0.8.4): admin-triggered
Send Test Notificationon each Telegram channel โ exactly one event + one delivery + one message through the existing factory/sender/encrypted-secret pipeline, explicitly labelledTest 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(defaultUTC) selected in the channel edit form; the Telegram message timestamp renders asYYYY-MM-DD HH:mm:ss (Timezone)viaIntl.DateTimeFormat(DST-aware), internal storage stays UTC - Rule-based delivery matching (global or per-domain rules, by source / event type โ including the
expiration_reminderevent 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 legacyTELEGRAM_BOT_TOKENenv fallback - ๐ฌ Delivery history with status tracking (pending / sending / sent / failed) and manual retry
- 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
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 emitsexpiration_reminderevents (sourceexpiration) for domains whose reminder day has arrived, deduplicated so a domain is reminded once per day - ๐ Event โ Delivery together:
insertEventsAndGenerateDeliveriescreates 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
sendingrecovery (crash-safe) and concurrent-worker safety (SQLite CAS)
- English / ็ฎไฝไธญๆ language switching in the header
- Locale-aware UI dictionary; preference stored in the
domain-monitor-localecookie (en/zh-CN, defaulten) - 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
flowchart LR
A[Domain Check] --> B[Event]
B --> C[Rule Matching]
C --> D[Delivery Queue]
D --> E[Worker / Cron]
E --> F[Telegram / Webhook / Email]
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. ๐ช
- ๐ 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 byENCRYPTION_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+deliveryIdfor receiver-side deduplication
- ๐งช 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 +
Intlrendering - ๐งช 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 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_reminderevents - โ 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
getMeverification) - ๐ 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.
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. ๐
pnpm worker # one tick, up to 50 pending deliveries
pnpm worker --limit 10 # cap this tick at 10 deliveries
pnpm worker --limit=10 # sameThe 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โ stalesendingdeliveries moved back topending(crash recovery)attemptedโ deliveries this tick tried to deliversent/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.
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.
- 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
pendingdeliveries (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 stableeventId+deliveryIdin 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
pendingqueue. - SQLite
busy_timeout = 5000is enabled so the worker and the web app can write concurrently without immediateSQLITE_BUSYfailures. - 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.
pnpm testCurrent 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 buildUI (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. ๐
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:migrateonly 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.jsoncBoth paths share the migration files in
src/db/migrations/, but do not usepnpm db:migrateto migrate D1. Full flow: see Cloudflare Production Deployment (Option B).
- 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.DateTimeFormatrendering, 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
nolockmount 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 SQLite remains on local
/tmpoverlay:/tmp/domain-monitor/data/domain-monitor.db. - โ๏ธ Production backups are stored on NFS persistent storage (daily, 7-day retention).
โ ๏ธ WARNING: the current NFSv3 +nolockmount 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.
Contributions are welcome! ๐
pnpm install
pnpm test
pnpm lint
pnpm buildSee CONTRIBUTING.md for the full contribution guide.


