Skip to main content

uptime-monitor-onboarding

Why It Exists

The skill exists because of a real production incident: yellow-pages-api's DB pooler went ECONNREFUSED, every real route 500'd, and its /health check — liveness-only, no DB call — reported healthy the entire time. UptimeRobot never paged, because the thing it was checking never actually tested the thing that broke. The skill's core discipline follows directly from that gap: never trust that a commit, a build, or a "deploy succeeded" status means the live service reflects it — curl the real endpoint before registering a monitor for it.

That discipline earned its place a second time, independently, the same afternoon during the original rollout: cates-works-api and offerly-api both had /health/deep fully committed and merged to main, but each service's image-backed Render deploy was a snapshot built before that commit landed, so the live route 404'd indefinitely with no error surfaced anywhere. Both repos hit the identical trap with no coordination between the two — which is why "an image-backed deploy is a snapshot, not a mirror" is called out as this skill's single most repeated failure mode, not a one-off footnote.

The skill was added in a dedicated commit (2a1ae3d, "feat(skills): add uptime-monitor-onboarding skill"). Its adaptation notes — Prisma vs Drizzle, Fastify vs Hono, a Supabase-JS-only service with no DATABASE_URL, auth middleware that 401s an unlisted route before it ever reaches a DB check — accumulated across onboarding 16+ services in this portfolio, per its own metadata, and the ECR-cutover steps deliberately route to the sibling docker-ecr-render-migration skill rather than re-deriving that portfolio's AWS/IAM specifics inline, so this skill stays generic enough to travel with the next-starters template into future scaffolds. A later, unrelated docker-ecr-render-migration session (docs/sessions/2026-08-23-ecr-render-migration-completion/SUMMARY.md) independently confirms this skill's generate-skill-docs.mjs --check deep-dive-page gap was already pre-existing at that point, not something introduced by that session's own work.

What It Does

Turns a live-or-about-to-be-live service into one actually covered by UptimeRobot, branching on whether it has a database at all:

  • Has a database: needs GET /health/deep — a real DB round-trip returning 200 {status:'ok', db:'ok'} on success or 503 {status:'error', db:'error'} on failure, distinct from a plain /health that only proves the process is alive. Adds or confirms the route (reference implementation: Hono + Drizzle in templates/dashboard/apps/api/src/health-deep.ts), adapting per-repo for whatever ORM, framework, and auth-middleware shape actually exists there rather than assuming the reference applies unmodified.
  • No database (a stateless proxy, a static-file server): just needs its existing /health to actually work — kind: "health-plain".
  • No HTTP endpoint at all (a background worker, cron job, queue consumer): stops here and flags it as a known gap. UptimeRobot fundamentally cannot monitor a process with no URL to check; that needs an unbuilt heartbeat/dead-man's-switch pattern instead.

For a database-backed service already on (or moving to) an image-backed Render deploy, it also walks the ECR rebuild-push-cutover sequence: the exact Render API PATCH body that actually works (image:{ownerId, registryCredentialId, imagePath}, registryCredentialId as a plain string) after documenting every flatter or differently-nested shape that silently 200s and no-ops instead, followed by the explicit POST /deploys an image-backed service needs since a config PATCH alone never triggers a redeploy. It closes by registering the monitor via scripts/register-uptime-monitors.mjs (idempotent, --dry-run and --only flags, matches by friendlyName so reruns never duplicate) and confirming against UptimeRobot's real API rather than trusting a 0 exit code.

How To Use It

Triggers on: "monitor this service", "add uptime monitoring", "service is now deployed via docker", "register a monitor", "the ECR cutover is live", "health check for uptime", or onboarding any new API to this portfolio's UptimeRobot account.

skills add git@github.com:catesandrew/next-starters.git --skill skills/uptime-monitor-onboarding -g

Gotchas & Invariants

  • Don't monitor an existing auth-gated route by treating its 401 as healthy. customHTTPStatuses (matching a specific non-2xx as "up") is Pro-only on UptimeRobot. A free-plan monitor treats any non-2xx as DOWN, so this pages constantly on a perfectly healthy service — /health/deep sidesteps it entirely by always returning a plain 200 on success.
  • Auth middleware can 401 the new route before it ever reaches the DB check, silently defeating the whole point, when it gates every unlisted path globally instead of scoping to an /api/*-style prefix. Check the real middleware chain and add /health/deep to its allowlist if one exists.
  • An image-backed Render deploy is a snapshot, not a mirror. A route fully committed and merged to main means nothing to the live container until something explicitly rebuilds, re-pushes, and re-points it. If the repo is already ECR-cutover, assume the image is stale until proven otherwise — rebuilding is not something to skip "because the code is already committed."
  • The only Render PATCH body that actually works is the nested oneimage:{ownerId, registryCredentialId, imagePath} with registryCredentialId as a plain string. A flat top-level imagePath plus registryCredential:{"id": ...} is what a GET on an image-backed service returns, not what PATCH accepts — it silently 200s and no-ops.
  • A PATCH alone never redeploys an image-backed service. An explicit POST /v1/services/{serviceId}/deploys is required every time, then poll until the deploy status leaves update_in_progress/ build_in_progress.
  • A 200 status alone isn't proof the route works. Print the response body and confirm it's the real {"status":"ok","db":"ok"}, not some unrelated catch-all route silently returning 200.
  • The alert down-confirmation threshold is silently coerced to 0 on UptimeRobot's free plan — sending threshold: 10 is accepted (200) but the created monitor comes back with threshold: 0, so every monitor pages on the first failed check with no grace period for a normal deploy cold-start. Always read back the created resource to confirm a plan-gated field actually stuck.
  • Use the v3 API, not v2api.uptimerobot.com/v2's newMonitor can return access_denied even for a minimal payload depending on account/plan state, and this portfolio's account hit it.
  • --only on the registration script is not optional discipline — running it without a scope registers every entry in the shared config, including any unverified-live services, as permanent false-DOWN monitors.
  • Status-page updates via PATCH replace the monitorIds array, they don't append to itGET the page first to read the current array before sending the update, and use PATCH, not PUT (PUT 404s on that endpoint).
  • docker-ecr-render-migration — the authoritative skill for this portfolio's actual AWS/IAM-specific ECR cutover steps. This skill's own ECR section is a condensed version for when a repo is already cutover and only needs an image refresh; it deliberately routes to that skill instead of re-deriving account-specific conventions inline, keeping this skill generic enough to travel with the next-starters template into future scaffolds.

Sourced from: skills/uptime-monitor-onboarding/metadata.json, skills/uptime-monitor-onboarding/SKILL.md, templates/dashboard/docs/UPTIME-MONITORING.md, docs/sessions/2026-08-23-ecr-render-migration-completion/SUMMARY.md, git log --diff-filter=A -- skills/uptime-monitor-onboarding/SKILL.md (commit 2a1ae3d).