I manage 30+ indie web products across Netlify, Supabase, and GitHub. I built thehelm.fyi from scratch to monitor all of it: Express 5, EJS, HTMX 2.x, Tailwind 4 CLI, deployed on Fly.io. Hand-rolled fleet dashboard. Real problem, real solution.
Wanted to try n8n so I rebuilt it there this afternoon.
A single workflow runs daily at midnight. Four data sources fire in parallel:
- Uptime monitoring: HTTP pings to 22 production URLs, checking for 200 responses
- Netlify deployments: Queries all 30+ sites for latest deploy status and timestamps
- Supabase user counts: Pulls profile counts from 5 auth-enabled apps via REST API
- GitHub activity: Fetches all repositories, last push dates, open issues
Everything merges into a single stream. Claude (Sonnet) synthesizes the raw data into a fleet health report: what's healthy, what's stale, what needs attention, and the top 3 actions for the day.
thehelm.fyi works. But it took a while to build and requires a running server, a deployment pipeline, ongoing maintenance, and context-switching into a dashboard to check fleet status. I wanted to try n8n, and had previously kicked around the idea of coding up some Claude API tooling to just give me a couple sentences instead of needing to remember to check my local helm dashboard, but there's a lot of plumbing there. Cron job, Slack integration or Resend config or whatever.
The n8n workflow took an afternoon. It runs autonomously. It produces a smarter report because Claude can synthesize across data sources in ways that templated HTML can't. And it's extensible without a deployment: Add a Slack node for alerts, an Ollama node for local AI triage, an MCP Server trigger so Claude Code can query fleet status from the terminal. Pretty sure all that's doable.
The platform eliminated the infrastructure tax. The workflow is the product.
[Scheduled Trigger]
(Midnight, daily)
|
+-------------+-------------+--------------+
| | | |
[Get domains] [Netlify API] [Supabase API] [GitHub API]
| | | |
[HTTP Requests] | | |
(22 prod URLs) | | |
| | | |
+-------------+--------------+--------------+
|
[Merge]
(41 items)
|
[Claude]
(Sonnet)
|
[Report]
The workflow JSON is in workflows/fleet-helm.json (credentials replaced with placeholders). Here's what's inside the Code nodes.
Generates one item per production URL. The HTTP Request node downstream executes once per item automatically.
const sites = [
{ name: 'centricle', url: 'https://centricle.com' },
{ name: 'mileweave', url: 'https://mileweave.us' },
{ name: 'picweave', url: 'https://picweave.us' },
{ name: 'batteries', url: 'https://batteries.fyi' },
{ name: 'cables', url: 'https://cables.fyi' },
{ name: 'fasteners', url: 'https://fasteners.fyi' },
{ name: 'iron', url: 'https://iron.fyi' },
{ name: 'mahogany', url: 'https://mahogany.fyi' },
{ name: 'wings', url: 'https://wings.fyi' },
{ name: 'packlife', url: 'https://thepacklife.us' },
{ name: 'yappyhour', url: 'https://yappyhour.us' },
{ name: 'thoughtstream', url: 'https://thoughtstream.us' },
{ name: 'thoughtweave', url: 'https://thoughtweave.us' },
{ name: 'dipshit', url: 'https://dipshit.fyi' },
{ name: 'doomscrolling', url: 'https://doomscrolling.us' },
{ name: 'flailspin', url: 'https://flailspin.com' },
{ name: 'whoami', url: 'https://whoami.fyi' },
{ name: 'splitsmart', url: 'https://splitsmart.us' },
{ name: 'choremode', url: 'https://choremode.us' },
{ name: 'propdock', url: 'https://propdock.us' },
{ name: 'verbatim', url: 'https://verbatim.fyi' },
{ name: 'tidewaterweb', url: 'https://tidewaterweb.com' },
];
return sites.map(site => ({ json: site }));Hits the Netlify API directly to pull all sites and their latest deploy. The native Netlify node only handles one site at a time; this gets all 30+ in a single Code node.
const token = process.env.NETLIFY_TOKEN; // configured in n8n credentials
const sitesRes = await fetch('https://api.netlify.com/api/v1/sites?per_page=100', {
headers: { 'Authorization': `Bearer ${token}` },
});
const sites = await sitesRes.json();
const results = [];
for (const site of sites) {
const deploysRes = await fetch(
`https://api.netlify.com/api/v1/sites/${site.id}/deploys?per_page=1`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const deploys = await deploysRes.json();
const latest = deploys[0] || {};
results.push({
json: {
name: site.name,
url: site.ssl_url || site.url,
lastDeploy: latest.created_at || 'never',
deployStatus: latest.state || 'unknown',
updatedAt: site.updated_at,
},
});
}
return results;Queries the profiles table across multiple Supabase projects using HEAD requests with Prefer: count=exact. Each project has its own URL and service role key.
const projects = [
{ name: 'mileweave', url: 'https://YOUR_PROJECT.supabase.co', key: 'YOUR_SERVICE_KEY' },
{ name: 'picweave', url: 'https://YOUR_PROJECT.supabase.co', key: 'YOUR_SERVICE_KEY' },
// ... additional projects
];
const results = [];
for (const project of projects) {
try {
const response = await fetch(
`${project.url}/rest/v1/profiles?select=count`,
{
method: 'HEAD',
headers: {
'apikey': project.key,
'Authorization': `Bearer ${project.key}`,
'Prefer': 'count=exact',
},
}
);
const count = response.headers.get('content-range')?.split('/')[1] || '0';
results.push({ json: { name: project.name, userCount: parseInt(count), status: 'ok' } });
} catch (err) {
results.push({ json: { name: project.name, userCount: 0, status: 'error', error: err.message } });
}
}
return results;All 41 items from the Merge node are passed to Claude Sonnet as raw JSON. The node runs with "Execute Once" enabled so it makes a single API call instead of 41.
You are a fleet operations analyst for ksmith's portfolio of 30+ indie web
products. Analyze the following aggregated data and produce a concise fleet
health report.
## Raw Fleet Data
{{ JSON.stringify($input.all().map(item => item.json), null, 2) }}
Produce a report with:
1. **Fleet Overview**: Total sites, how many healthy vs stale vs down
2. **Attention Needed**: Sites with failed deploys, sites not deployed in 30+ days, any downtime
3. **Growth Signals**: User count changes, active repos, recent deploys
4. **Recommendation**: Top 3 actions for today
Be direct and concise. No fluff.
From a live run on April 5, 2026:
Fleet Overview: 30 properties analyzed. 26 healthy (87%), 3 stale (10%), 1 down (3%).
Attention Needed: thehelm.fyi showing SSL timeout (Fly.io instance sleeping). Thoughtweave still on coming-soon page. Multiple CLI tools active in repos but low public visibility.
Growth Signals: batteries.fyi, cables.fyi, fasteners.fyi showing consistent updates through March 2026. 8 repositories pushed within last 30 days. New projects (thehelm, package-stacker, deink) recently created.
Recommendations: (1) Fix thehelm.fyi deployment. (2) Decide on Thoughtweave launch timeline. (3) Use the Helm dashboard to automate this fleet health analysis.
These are real next steps, not hand-waving. The architecture supports all of them without restructuring.
- Ollama node for local AI classification (dormant/healthy/critical) where latency and cost matter more than synthesis quality. Already running Ollama locally.
- Conditional routing after classification: site-down alerts go to Slack immediately, dormant flags get batched into a weekly digest, growth signals get logged.
- MCP Server trigger exposing the fleet status as a tool. Claude Code calls
fleet_statusfrom the terminal and gets a live report. n8n becomes invisible infrastructure inside the dev workflow. - Heritage Radar (second workflow): RSS feeds from classic car, boat, and aviation auction sites, classified by AI into content buckets for iron.fyi, mahogany.fyi, and wings.fyi. Human-in-the-loop approval via Slack before committing to GitHub and auto-deploying.
# docker-compose.yml
services:
n8n:
image: docker.n8n.io/n8nio/n8n
ports:
- "5678:5678"
volumes:
- n8n_data:/home/node/.n8n
environment:
- N8N_HOST=n8n.test
- WEBHOOK_URL=http://n8n.test
- N8N_SECURE_COOKIE=false
volumes:
n8n_data:Running via OrbStack on macOS with Caddy reverse proxy at http://n8n.test.
Built by ksmith
MIT
