Skip to content

T 170


uid: T-170 title: Vercel Hobby downgrade β€” unblock deploys (crons β†’ NAS scheduler, maxDuration ≀ 60s) status: done area: infra-deploy created: 2026-07-21 updated: 2026-07-21 owner: girafeev1 assignee: Records (Infrastructure) related: T-147, T-084, T-091


Goal (owner, 2026-07-21)

The Vercel team was intentionally downgraded Pro β†’ Hobby (owner confirmed; effective ~2026-07-16 β€” the last successful production deploy). Since then EVERY deploy of main is rejected before building: first on the vercel.json cron (30 */6 * * * β€” Hobby allows daily-only crons; deploy-hook jobs die SILENTLY, the API surfaces cron_jobs_limits_reached), and next on maxDuration 120–300s declarations (Hobby caps functions at 60s). Owner: keep Hobby; cron jobs fire from the NAS's scheduled task instead; verify the account still builds.

What shipped

  • vercel.json: crons block removed β€” the scrape-heartbeat is no longer Vercel-scheduled.
  • maxDuration clamped to 60 (the Hobby ceiling, with a constraint comment) on 8 routes: 3Γ— records download-zip (were 300), 4Γ— invoice/WOPC/doc pdf (were 120), docs/view (was 120).
  • No endpoint/auth changes β€” /api/cron/scrape-heartbeat still enforces CRON_SECRET (fails closed).

NAS scheduled task (owner action β€” replaces the Vercel cron)

Schedule on the NAS (Synology Task Scheduler), same cadence as before (every 6h at :30): 30 */6 * * * β†’ curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://eop.theestablishers.com/api/cron/scrape-heartbeat (CRON_SECRET = the app's existing env value β€” GCP Secret Manager aote-pms-env bundle; never inline it in the task's command where logs echo it β€” read it from a root-only file on the NAS.) - ⚠️ Blind-spot note (flagged, owner's call): the heartbeat watches the NAS scrapers, and its own header says a watchdog must not live on the box it watches. Scheduling the TRIGGER from the NAS means a fully-dead NAS also kills the alarm that would report it. Cheap off-box alternative if ever wanted: a GitHub Actions schedule: workflow curling the same endpoint.

Functional impact of the 60s clamp (accepted trade-off)

  • Batch ZIP exports (project/coaching/WOPC) had 300s headroom; big batches may now hit 60s timeouts on Vercel. The remote-render path (Cloud Run, isRemoteRenderConfigured) offloads the heavy Chromium work where configured and is the mitigation; if 504s appear on large exports, route more of the pipeline remote rather than raising the cap (raising it fails every deploy on Hobby).
  • Single-PDF renders (120β†’60) were comfortably <60s in practice.

Log

  • 2026-07-21 β€” created + done (Records Infrastructure; owner decision in-session: downgrade is intentional, crons move to the NAS scheduler). βœ… Read AGENTS.md Β· checked the board by scope (T-147 owns the heartbeat's semantics β€” unchanged here; T-084/T-091 are the NAS deploy runbooks; no task covers the Hobby-plan deploy block; no dup). Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Diagnosis trail: deploy-hook fires returned 201 but no deployment ever materialized (silent); the API create (AGENTS.md fallback) surfaced the real 400 cron_jobs_limits_reached. maxDuration audit found the second blocker before it could burn another deploy cycle.
  • Verdict: merged to main; the post-merge production deploy is the live proof of whether Hobby still builds β€” result recorded in-session (deploy fired immediately after merge, owner order).
  • Blast-radius: vercel.json (no more Vercel-scheduled cron β€” the heartbeat DOES NOT RUN until the owner creates the NAS task above), 8 API routes' maxDuration. Any agent tempted to raise maxDuration above 60 or re-add a sub-daily Vercel cron will break every deploy β€” don't.