name: cloudflare-uhoo-poller description: Use when implementing or debugging the uHoo continuous monitoring poller running on Cloudflare Workers with Supabase
Cloudflare uHoo Poller
Overview
A Cloudflare Worker that polls the uHoo API every minute and stores readings in Supabase. Replaced the previous Supabase Edge Function + pg_cron approach due to cold start latency (5-10 min gaps).
Why Cloudflare Workers: 10ms cold starts, exact 60s intervals, free tier, completely independent of the FastAPI app.
Core principle: Poll all active devices, deduplicate by timestamp, store CmReading + CmFinding rows.
Architecture
Cloudflare Cron Trigger (every 60s)
↓
Worker fetches devices from Supabase (device_connection)
↓
For each device (in parallel):
├─ Fetch latest reading from uHoo API
├─ Dedup check: skip if timestamp already in cm_reading
├─ Insert cm_reading rows (with tenant_id string)
├─ Evaluate thresholds → insert cm_finding rows
└─ Update last_poll_at
Quick Reference
| File | Purpose |
|---|---|
poller/src/index.ts |
Worker entry point + polling logic |
poller/src/uhoo.ts |
uHoo API client (token mgmt + parsing) |
poller/src/thresholds.ts |
Threshold evaluation (GOOD/WATCH/CRITICAL) |
poller/wrangler.toml |
Cloudflare config + cron trigger |
poller/package.json |
Dependencies (just @supabase/supabase-js) |
Key Patterns
Deduplication by Timestamp
The uHoo API returns the latest reading only. If the cron fires but the device hasn't produced a new reading since the last poll, skip the insert:
// Check if this exact timestamp already exists
const { count } = await supabase
.from('cm_reading')
.select('*', { count: 'exact', head: true })
.eq('device_id', device.id)
.eq('reading_timestamp', readingTs.toISOString())
if (count > 0) return // Already stored, skip
tenant_id Must Be String
The cm_reading table is partitioned by month. The tenant_id column must be a string, not UUID:
// Site returns UUID, must convert to string
const tenantId = String(site.tenant_id)
Parallel Device Polling
Poll all devices in parallel to stay within the 30s Worker timeout:
const results = await Promise.allSettled(
devices.map(d => pollDevice(d, tenantMap))
)
Common Mistakes
| Mistake | Fix |
|---|---|
Using limit=1 and missing data |
Fetch all devices in parallel, each gets its own reading |
| Passing UUID to tenant_id | Convert: String(site.tenant_id) |
| No dedup → duplicate rows | Check timestamp before insert |
| Blocking on sequential polls | Use Promise.allSettled for parallel |
| Forgetting to update last_poll_at | Always update on success or error |
Deployment
cd poller
npm install
npx wrangler login
npx wrangler deploy
# Set secrets
npx wrangler secret put SUPABASE_URL
npx wrangler secret put SUPABASE_KEY
npx wrangler secret put UHOO_CLIENT_CODE
Debugging
# Check cron run logs
npx wrangler tail
# Verify cron is scheduled
npx wrangler deploy --dry-run
# Check Supabase for recent readings
curl -H "apikey: $SERVICE_KEY" \
"https://$PROJECT.supabase.co/rest/v1/cm_reading?order=reading_timestamp.desc&limit=5"