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 returning200 {status:'ok', db:'ok'}on success or503 {status:'error', db:'error'}on failure, distinct from a plain/healththat only proves the process is alive. Adds or confirms the route (reference implementation: Hono + Drizzle intemplates/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
/healthto 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
401as 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/deepsidesteps it entirely by always returning a plain200on 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/deepto its allowlist if one exists. - An image-backed Render deploy is a snapshot, not a mirror. A route
fully committed and merged to
mainmeans 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
PATCHbody that actually works is the nested one —image:{ownerId, registryCredentialId, imagePath}withregistryCredentialIdas a plain string. A flat top-levelimagePathplusregistryCredential:{"id": ...}is what aGETon an image-backed service returns, not whatPATCHaccepts — it silently 200s and no-ops. - A
PATCHalone never redeploys an image-backed service. An explicitPOST /v1/services/{serviceId}/deploysis required every time, then poll until the deploy status leavesupdate_in_progress/build_in_progress. - A
200status 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 returning200. - The alert down-confirmation threshold is silently coerced to
0on UptimeRobot's free plan — sendingthreshold: 10is accepted (200) but the created monitor comes back withthreshold: 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 v2 —
api.uptimerobot.com/v2'snewMonitorcan returnaccess_deniedeven for a minimal payload depending on account/plan state, and this portfolio's account hit it. --onlyon 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
PATCHreplace themonitorIdsarray, they don't append to it —GETthe page first to read the current array before sending the update, and usePATCH, notPUT(PUT404s on that endpoint).
Related Skills
- 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-starterstemplate 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).