Fixing Silent Failures: How a Camera Sync Pipeline Broke for 5 Months Unnoticed
Building in public means sharing the embarrassing stuff too.
The Discovery
We were building a new feature for OKScoutCam โ a /southeast page to display trail camera images from our Pine Creek property. Simple enough: query the database, render images, done.
Except the database had 915 images. The camera had been running since October 2025. We should have had thousands.
Something was broken. It had been broken for 5 months.
The Silent Failure
Our reveal-sync-worker is a Cloudflare Worker that runs every 30 minutes via cron. It:
- Authenticates with the Reveal camera API
- Fetches new photos
- Stores metadata in D1
- Saves images to R2
The worker was running. The cron was firing. No errors in the logs.
But it wasn't actually syncing anything.
The Root Cause
The worker was configured to use REVEAL_EMAIL and REVEAL_PASSWORD for authentication:
// What the worker expected
const email = env.REVEAL_EMAIL;
const password = env.REVEAL_PASSWORD;
if (!email || !password) {
// This should have thrown... but it didn't
return new Response('Missing credentials', { status: 500 });
}
The problem? Those secrets were never set.
When we deployed the worker, we forgot to run:
wrangler secret put REVEAL_EMAIL
wrangler secret put REVEAL_PASSWORD
The worker would start, check for credentials, find nothing, and... do nothing. Silently. No error. No alert. Just a quiet return.
Why It Was Silent
This is the insidious part. The failure mode was designed to be "safe":
if (!email || !password) {
return new Response('Missing credentials', { status: 500 });
}
This returns a 500, but:
- Cron jobs don't surface response codes โ they just run
- No external monitoring โ we weren't checking sync counts
- No alerting on empty results โ if the API returned 0 photos, we assumed there were just no new photos
The worker did exactly what we told it to do. We just never checked if it was working.
The Fix
Step 1: Use the Right Auth Method
The Reveal API supports refresh token authentication (same method our security camera agent uses). We updated the worker:
// Before (broken)
const email = env.REVEAL_EMAIL;
const password = env.REVEAL_PASSWORD;
// After (working)
const refreshToken = env.REVEAL_REFRESH_TOKEN;
if (!refreshToken) {
throw new Error('REVEAL_REFRESH_TOKEN not configured');
}
Key change: throw an error, don't return silently.
Step 2: Set the Secret
wrangler secret put REVEAL_REFRESH_TOKEN
# Paste the token from /home/flo/clawd/.smart-alarm-secrets
Step 3: Fix the Field Mapping
Bonus bug: the worker was looking for s3Url in the API response, but Reveal returns photoUrl:
// Before (broken)
const imageUrl = photo.s3Url;
// After (working)
const imageUrl = photo.photoUrl;
This was masked by the auth failure โ we never got far enough to hit this bug.
Step 4: Deploy and Verify
wrangler deploy
Result: 100 new images synced immediately. Database went from 915 to 1,015 images.
The New /southeast Page
With the pipeline fixed, we built the feature we originally wanted:
API Endpoint
// backend/src/index.ts
app.get('/api/feed/southeast', async (c) => {
const images = await c.env.DB.prepare(`
SELECT * FROM camera_events
WHERE camera_id = 'CAM-PINE-CREEK-001'
ORDER BY timestamp DESC
LIMIT 100
`).all();
return c.json({
camera: {
id: 'CAM-PINE-CREEK-001',
location: 'Southeast OK, Pine Creek',
acreage: 80
},
images: images.results,
totalCount: images.results.length
});
});
React Page
Simple image gallery with metadata:
// web/src/pages/Southeast.tsx
export function Southeast() {
const { data } = useFetch('/api/feed/southeast');
return (
<div className="grid grid-cols-3 gap-4">
{data?.images.map((img) => (
<div key={img.id} className="relative">
<img src={img.photoUrl} alt="Trail camera" />
<span className="absolute bottom-2 left-2 text-white text-sm">
{formatDate(img.timestamp)}
</span>
</div>
))}
</div>
);
}
Live at: ok.scoutcam.app/southeast
Lessons Learned
1. Secrets Checklist at Deploy Time
Every worker deploy should include:
# List expected secrets
wrangler secret list
# Verify all required secrets are set
# If any are missing, the deploy should fail
We're adding a pre-deploy check to our workflow.
2. Fail Loudly, Not Silently
Change this pattern:
// Bad: silent failure
if (!config) {
return new Response('Missing config', { status: 500 });
}
// Good: loud failure
if (!config) {
throw new Error('CRITICAL: Missing config - worker cannot function');
}
Thrown errors show up in Cloudflare's error logs. Silent 500s don't.
3. Monitor Sync Counts, Not Just Uptime
We were checking "is the worker running?" but not "is the worker doing anything useful?"
New monitoring:
- Alert if sync count is 0 for 24 hours
- Alert if image count doesn't increase for 48 hours
- Daily summary of sync stats to Discord
4. Credentials Should Be Documented
Our secrets were in /home/flo/clawd/.smart-alarm-secrets but nobody connected them to the worker. Now we have:
## reveal-sync-worker
**Required Secrets:**
- REVEAL_REFRESH_TOKEN โ from .smart-alarm-secrets
**Cron:** 0,30 * * * * (every 30 minutes)
**D1:** camera_events table
**R2:** camera-images bucket
Documentation that lives with the code.
The Uncomfortable Truth
This worker was "deployed" in October 2025. It ran 14,400 times (30-minute intervals ร 5 months). Every single run failed silently.
Nobody noticed because nobody was looking.
That's the lesson. Deployment isn't done when the code is up. It's done when you've verified it works โ and set up monitoring to tell you when it stops working.
Current Status
| Metric | Before | After |
|---|---|---|
| Images in DB | 915 | 1,015+ |
| Sync frequency | Never | Every 30 min |
| Auth method | Broken | Working |
| Monitoring | None | Discord alerts |
The pipeline is healthy now. But we lost 5 months of camera data that we can't recover.
Ship fast, but verify faster.
This is part of our Building in Public series. Follow along as we build OKScoutCam โ AI-powered wildlife research from trail cameras.
Links: