3 * Syncs domains from Cloudflare, Moniker, and eNom to local database every 5 minutes
4 * Uses Redis for caching domain data (10 minute TTL)
5 * Provides faster page loads and reduces API calls to registrars
8const Redis = require('ioredis');
9const pool = require('./db');
10const { listZones, getZone } = require('./services/cloudflare');
11const monikerService = require('./services/moniker');
12const enomService = require('./services/enom');
14const redisConfig = require('./config/redis');
15let redisClient = null;
17const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
18const REDIS_CACHE_TTL = 10 * 60; // 10 minutes in seconds
19const REDIS_KEY_PREFIX = 'domains:sync:';
20const API_DELAY = 500; // Delay between API calls to avoid rate limiting
23 * Initialize Redis connection
27 redisClient = new Redis({
29 maxRetriesPerRequest: null,
33 redisClient.on('error', (err) => {
34 console.error('[Domain Sync] Redis error:', err.message);
37 redisClient.on('connect', () => {
38 console.log('[Domain Sync] Redis connected');
41 return redisClient.connect().catch(err => {
42 console.error('[Domain Sync] Redis connection failed:', err.message);
47 * Sync domains from Cloudflare (DNS zones)
48 * Fetches all zones from Cloudflare API
49 * Returns a Map indexed by domain name for easy enrichment
51async function syncCloudflare() {
52 const startTime = Date.now();
53 console.log('[Domain Sync] Starting Cloudflare sync...');
56 const zones = await listZones();
58 if (!zones || zones.length === 0) {
59 console.log('[Domain Sync] No Cloudflare zones found');
63 // Create a Map indexed by domain name for fast lookup
64 const zoneMap = new Map();
66 zones.forEach(zone => {
67 zoneMap.set(zone.name.toLowerCase(), {
69 account_id: zone.account?.id,
71 nameservers: zone.name_servers || [],
72 development_mode: zone.development_mode,
73 plan: zone.plan?.name,
74 created_on: zone.created_on
78 const elapsed = Date.now() - startTime;
79 console.log(`[Domain Sync] Cloudflare sync complete: ${zoneMap.size} zones (${elapsed}ms)`);
82 console.error('[Domain Sync] Cloudflare sync error:', error.message);
88 * Check if nameservers are Cloudflare nameservers
89 * @param {Array} nameservers - Array of nameserver strings
90 * @returns {boolean} - True if Cloudflare nameservers detected
92function isCloudflareNameservers(nameservers) {
93 if (!Array.isArray(nameservers) || nameservers.length === 0) {
97 // Check if any nameserver contains cloudflare
98 return nameservers.some(ns =>
99 ns && ns.toLowerCase().includes('cloudflare')
104 * Sync domains from Moniker registrar
105 * Fetches ALL domains from Moniker API
106 * @param cloudflareZones
108async function syncMoniker(cloudflareZones) {
109 const startTime = Date.now();
110 console.log('[Domain Sync] Starting Moniker sync...');
113 // Fetch ALL domains from Moniker API (not from database)
114 const monikerDomains = await monikerService.getAllDomains();
116 if (monikerDomains.length === 0) {
117 console.log('[Domain Sync] No Moniker domains found in account');
121 console.log(`[Domain Sync] Found ${monikerDomains.length} Moniker domains from API`);
124 // Process each domain from Moniker
125 for (const domain of monikerDomains) {
127 // Rate limiting delay
128 await new Promise(resolve => setTimeout(resolve, API_DELAY));
130 // Get full domain details
131 const domainInfo = await monikerService.getDomainInfo(domain.name);
134 console.log(`[Domain Sync] Could not get info for ${domain.name}`);
138 // Build base domain object with Moniker registration data
139 const autoRenew = domain.auto_renew || domainInfo.raw?.AutoRenew === 'true';
140 console.log(`[Domain Sync] ${domain.name} - auto_renew from API: ${domain.auto_renew}, from raw: ${domainInfo.raw?.AutoRenew}, final: ${autoRenew}`);
144 registrar: 'moniker',
145 registrar_domain_id: domainInfo.raw?.DomainID || domain.name,
146 status: domainInfo.status || domain.status || 'unknown',
147 expiration_date: domainInfo.expiration_date,
148 registration_date: domainInfo.registration_date || domain.registration_date,
149 nameservers: domainInfo.nameservers || [],
151 domain_type: 'registered', // Moniker is a registrar - full domain management
152 auto_renew: autoRenew,
153 locked: domain.locked || domainInfo.raw?.Locked === 'true',
154 privacy_enabled: domainInfo.raw?.PrivacyEnabled === 'true'
158 // Check if domain uses Cloudflare nameservers
159 if (isCloudflareNameservers(domainObj.nameservers)) {
160 const cloudflareZone = cloudflareZones.get(domain.name.toLowerCase());
162 if (cloudflareZone) {
163 console.log(`[Domain Sync] Enriching ${domain.name} with Cloudflare zone data`);
165 // Enrich with Cloudflare zone data
166 domainObj.metadata.cloudflare_zone_id = cloudflareZone.zone_id;
167 domainObj.metadata.cloudflare_account_id = cloudflareZone.account_id;
168 domainObj.metadata.cloudflare_status = cloudflareZone.status;
169 domainObj.metadata.cloudflare_plan = cloudflareZone.plan;
170 domainObj.metadata.has_cloudflare_dns = true;
172 // Update nameservers from Cloudflare (more current)
173 if (cloudflareZone.nameservers && cloudflareZone.nameservers.length > 0) {
174 domainObj.nameservers = cloudflareZone.nameservers;
177 // Uses Cloudflare NS but no zone found (might be pending or external)
178 domainObj.metadata.has_cloudflare_dns = false;
179 console.log(`[Domain Sync] ${domain.name} uses Cloudflare NS but no zone found`);
183 domains.push(domainObj);
185 console.error(`[Domain Sync] Error processing Moniker domain ${domain.name}:`, err.message);
189 const elapsed = Date.now() - startTime;
190 console.log(`[Domain Sync] Moniker sync complete: ${domains.length}/${monikerDomains.length} domains (${elapsed}ms)`);
193 console.error('[Domain Sync] Moniker sync error:', error.message);
199 * Sync domains from eNom registrar
200 * Fetches ALL domains from eNom API
201 * @param cloudflareZones
203async function syncEnom(cloudflareZones) {
204 const startTime = Date.now();
205 console.log('[Domain Sync] Starting eNom sync...');
208 // Fetch ALL domains from eNom API (not from database)
209 const enomDomains = await enomService.getAllDomains();
211 if (enomDomains.length === 0) {
212 console.log('[Domain Sync] No eNom domains found in account');
216 console.log(`[Domain Sync] Found ${enomDomains.length} eNom domains from API`);
219 // Process each domain from eNom
220 for (const domain of enomDomains) {
222 // Rate limiting delay
223 await new Promise(resolve => setTimeout(resolve, API_DELAY));
225 // Get full domain details
226 const domainInfo = await enomService.getDomainInfo(domain.name);
229 console.log(`[Domain Sync] Could not get info for ${domain.name}`);
233 // Build base domain object with eNom registration data
234 const autoRenew = domain.auto_renew || domainInfo.auto_renew;
235 console.log(`[Domain Sync] ${domain.name} - auto_renew from API: ${domain.auto_renew}, from info: ${domainInfo.auto_renew}, final: ${autoRenew}`);
240 registrar_domain_id: domainInfo.raw?.domainnameid || domain.name,
241 status: domainInfo.status || domain.status || 'unknown',
242 expiration_date: domainInfo.expiration_date || domain.expiration_date,
243 registration_date: domainInfo.registration_date || domain.registration_date,
244 nameservers: domainInfo.nameservers || [],
246 domain_type: 'registered', // eNom is a registrar - full domain management
247 auto_renew: autoRenew,
248 locked: domain.locked || domainInfo.raw?.['registrar-lock'] === '1',
249 privacy_enabled: domainInfo.raw?.['whois-protection'] === 'Enabled'
253 // Check if domain uses Cloudflare nameservers
254 if (isCloudflareNameservers(domainObj.nameservers)) {
255 const cloudflareZone = cloudflareZones.get(domain.name.toLowerCase());
257 if (cloudflareZone) {
258 console.log(`[Domain Sync] Enriching ${domain.name} with Cloudflare zone data`);
260 // Enrich with Cloudflare zone data
261 domainObj.metadata.cloudflare_zone_id = cloudflareZone.zone_id;
262 domainObj.metadata.cloudflare_account_id = cloudflareZone.account_id;
263 domainObj.metadata.cloudflare_status = cloudflareZone.status;
264 domainObj.metadata.cloudflare_plan = cloudflareZone.plan;
265 domainObj.metadata.has_cloudflare_dns = true;
267 // Update nameservers from Cloudflare (more current)
268 if (cloudflareZone.nameservers && cloudflareZone.nameservers.length > 0) {
269 domainObj.nameservers = cloudflareZone.nameservers;
272 // Uses Cloudflare NS but no zone found
273 domainObj.metadata.has_cloudflare_dns = false;
274 console.log(`[Domain Sync] ${domain.name} uses Cloudflare NS but no zone found`);
278 domains.push(domainObj);
280 console.error(`[Domain Sync] Error processing eNom domain ${domain.name}:`, err.message);
284 const elapsed = Date.now() - startTime;
285 console.log(`[Domain Sync] eNom sync complete: ${domains.length}/${enomDomains.length} domains (${elapsed}ms)`);
288 console.error('[Domain Sync] eNom sync error:', error.message);
294 * Process DNS-only Cloudflare zones
295 * Creates domain entries for Cloudflare zones that are not registered with Moniker/eNom
296 * @param cloudflareZones
297 * @param monikerDomains
300function processDnsOnlyZones(cloudflareZones, monikerDomains, enomDomains) {
301 const registeredDomains = new Set();
303 // Collect all registered domain names
304 monikerDomains.forEach(d => registeredDomains.add(d.name.toLowerCase()));
305 enomDomains.forEach(d => registeredDomains.add(d.name.toLowerCase()));
307 const dnsOnlyDomains = [];
309 // Find Cloudflare zones that are not registered with our registrars
310 cloudflareZones.forEach((zoneData, domainName) => {
311 if (!registeredDomains.has(domainName)) {
312 // This is a DNS-only zone (not registered through our system)
313 dnsOnlyDomains.push({
315 registrar: 'cloudflare',
316 registrar_domain_id: zoneData.zone_id,
317 status: zoneData.status === 'active' ? 'active' : 'inactive',
318 expiration_date: null, // DNS-only zones don't have expiration dates
319 nameservers: zoneData.nameservers || [],
320 created_date: zoneData.created_on,
322 domain_type: 'dns_only', // DNS management only, not registered through our platform
323 zone_id: zoneData.zone_id,
324 account_id: zoneData.account_id,
325 development_mode: zoneData.development_mode,
332 console.log(`[Domain Sync] Found ${dnsOnlyDomains.length} DNS-only Cloudflare zones`);
333 return dnsOnlyDomains;
337 * Update database with synced domain data
341async function updateDatabase(registrar, domains) {
342 if (domains.length === 0) return;
344 console.log(`[Domain Sync] Updating database with ${domains.length} ${registrar} domains...`);
348 for (const domain of domains) {
350 // Check if domain exists
351 const existingDomain = await pool.query(
352 `SELECT domain_id FROM domains WHERE domain_name = $1`,
356 if (existingDomain.rows.length > 0) {
357 // Update existing domain
358 const metadataJson = JSON.stringify(domain.metadata);
359 console.log(`[Domain Sync] Updating ${domain.name} with metadata:`, metadataJson);
364 registrar_domain_id = $2,
365 expiration_date = $3,
369 WHERE domain_name = $6`,
372 domain.registrar_domain_id,
373 domain.expiration_date,
374 JSON.stringify(domain.nameservers),
381 // Insert new domain (assign to root tenant)
382 const tenantResult = await pool.query(
383 `SELECT tenant_id FROM tenants WHERE subdomain IN ('admin', 'everydaytech', 'root') OR is_msp = true ORDER BY tenant_id LIMIT 1`
386 if (tenantResult.rows.length > 0) {
387 const tenantId = tenantResult.rows[0].tenant_id;
390 `INSERT INTO domains (
391 tenant_id, domain_name, registrar, registrar_domain_id,
392 status, expiration_date, nameservers, metadata,
393 registration_date, created_at, updated_at
394 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())`,
399 domain.registrar_domain_id,
401 domain.expiration_date,
402 JSON.stringify(domain.nameservers),
403 JSON.stringify(domain.metadata),
404 domain.created_date || null
411 console.error(`[Domain Sync] Error updating domain ${domain.name}:`, err.message);
415 console.log(`[Domain Sync] Database updated: ${updated} updated, ${inserted} inserted`);
419 * Main sync orchestrator
420 * New architecture: Two-way sync with enrichment
421 * 1. Fetch ALL domains from each registrar API
422 * 2. Fetch ALL Cloudflare zones
423 * 3. Enrich registered domains with Cloudflare DNS data if they use Cloudflare NS
424 * 4. Process DNS-only Cloudflare zones
425 * 5. Update database and cache
427async function syncAllDomains() {
428 const startTime = Date.now();
429 console.log('[Domain Sync] ========================================');
430 console.log('[Domain Sync] Starting two-way domain sync');
431 console.log('[Domain Sync] ========================================');
434 // Ensure Redis is connected
435 if (!redisClient || redisClient.status !== 'ready') {
439 // Step 1: Fetch Cloudflare zones first (needed for enrichment)
440 console.log('[Domain Sync] Step 1: Fetching Cloudflare zones...');
441 const cloudflareZones = await syncCloudflare();
443 // Step 2: Sync from registrars and enrich with Cloudflare data
444 console.log('[Domain Sync] Step 2: Syncing registrars with Cloudflare enrichment...');
445 const [monikerDomains, enomDomains] = await Promise.all([
446 syncMoniker(cloudflareZones),
447 syncEnom(cloudflareZones)
450 // Step 3: Process DNS-only Cloudflare zones (not registered with our registrars)
451 console.log('[Domain Sync] Step 3: Processing DNS-only Cloudflare zones...');
452 const dnsOnlyDomains = processDnsOnlyZones(cloudflareZones, monikerDomains, enomDomains);
454 // Step 4: Update database for all domain types
455 console.log('[Domain Sync] Step 4: Updating database...');
457 updateDatabase('moniker', monikerDomains),
458 updateDatabase('enom', enomDomains),
459 updateDatabase('cloudflare', dnsOnlyDomains)
462 // Step 5: Cache results in Redis with TTL
463 if (redisClient && redisClient.status === 'ready') {
464 console.log('[Domain Sync] Step 5: Caching results in Redis...');
467 `${REDIS_KEY_PREFIX}moniker`,
469 JSON.stringify(monikerDomains)
472 `${REDIS_KEY_PREFIX}enom`,
474 JSON.stringify(enomDomains)
477 `${REDIS_KEY_PREFIX}cloudflare`,
479 JSON.stringify(dnsOnlyDomains)
483 // Store sync summary with enrichment stats
484 const enrichedMoniker = monikerDomains.filter(d => d.metadata.has_cloudflare_dns).length;
485 const enrichedEnom = enomDomains.filter(d => d.metadata.has_cloudflare_dns).length;
488 lastSync: new Date().toISOString(),
490 count: monikerDomains.length,
491 cloudflareEnriched: enrichedMoniker
494 count: enomDomains.length,
495 cloudflareEnriched: enrichedEnom
498 count: dnsOnlyDomains.length,
499 dnsOnly: dnsOnlyDomains.length
502 registered: monikerDomains.length + enomDomains.length,
503 dnsOnly: dnsOnlyDomains.length,
504 all: monikerDomains.length + enomDomains.length + dnsOnlyDomains.length,
505 enriched: enrichedMoniker + enrichedEnom
509 await redisClient.setex(
510 `${REDIS_KEY_PREFIX}summary`,
512 JSON.stringify(summary)
515 console.log('[Domain Sync] Results cached in Redis');
518 const elapsed = Date.now() - startTime;
519 const totalDomains = monikerDomains.length + enomDomains.length + dnsOnlyDomains.length;
520 const enrichedCount = monikerDomains.filter(d => d.metadata.has_cloudflare_dns).length +
521 enomDomains.filter(d => d.metadata.has_cloudflare_dns).length;
523 console.log('[Domain Sync] ========================================');
524 console.log(`[Domain Sync] Two-way sync complete: ${totalDomains} total domains (${elapsed}ms)`);
525 console.log(`[Domain Sync] - Moniker (registered): ${monikerDomains.length}`);
526 console.log(`[Domain Sync] - eNom (registered): ${enomDomains.length}`);
527 console.log(`[Domain Sync] - Cloudflare (DNS-only): ${dnsOnlyDomains.length}`);
528 console.log(`[Domain Sync] - Enriched with Cloudflare DNS: ${enrichedCount}`);
529 console.log('[Domain Sync] ========================================');
532 console.error('[Domain Sync] Error during sync:', error);
537 * Start the sync worker
539function startSyncWorker() {
540 console.log(`[Domain Sync] Worker started - two-way sync every ${SYNC_INTERVAL / 1000 / 60} minutes`);
541 console.log('[Domain Sync] Architecture:');
542 console.log('[Domain Sync] 1. Fetch domains from Moniker, eNom, Cloudflare APIs');
543 console.log('[Domain Sync] 2. Enrich registered domains with Cloudflare DNS data');
544 console.log('[Domain Sync] 3. Track DNS-only Cloudflare zones separately');
545 console.log('[Domain Sync] Redis caching enabled (10 min TTL)');
547 // Initialize Redis and run first sync
550 console.log('[Domain Sync] Running initial two-way sync...');
551 return syncAllDomains();
554 // Schedule periodic sync
555 setInterval(syncAllDomains, SYNC_INTERVAL);
556 console.log('[Domain Sync] Periodic two-way sync scheduled');
559 console.error('[Domain Sync] Startup error:', err);
563// Start the worker if this file is run directly
564if (require.main === module) {
568module.exports = { startSyncWorker, syncAllDomains };