Skip to main content

Background Jobs

The routes under /api/jobs/* are machine-invoked endpoints, not part of the customer/admin API. They are called by Upstash QStash — either on a fixed schedule or enqueued with a delay when a business event occurs — and they perform the platform’s background automations: reminders, report emails, nudges, follow-ups, and stale-state checks. They are hosted in apps/admin and served from admin.theroyalglow.in/api/jobs/*.
Base URL: admin.theroyalglow.in · Auth: Upstash-Signature header (no session). These are POST endpoints invoked by QStash, not by browsers or API clients. Each route reads the raw request body once, verifies the Upstash-Signature header via verifyQStashSignature, and returns 401 Unauthorized if verification fails. They deliberately do not use the { success, data } envelope or withErrorHandler — instead they return a minimal { "success": true, ... } JSON on success and a non-2xx on failure so QStash retries. On success each route pings a BetterStack heartbeat. External integrations (Resend, Brevo, web-push, Slack) are guarded extension points — when their keys are absent the send is a no-op
  • log, and the job still completes and returns 200.

Job summary

All 19 routes are POST. “Scheduled” jobs are fired by a QStash schedule; “Triggered” jobs are enqueued with a delay by an application event. There are 14 scheduled and 5 triggered QStash routes under /api/jobs/*. One further automation — the pprd DB sync — runs as a GitHub Actions cron (not an HTTP route, so it is not listed here). Schedules and cadences are from background-jobs.md; times shown in IST.

Invocation contract

Every job route follows the same thin orchestration shape:
1

Method

The route accepts only POST.
2

Signature verification

The raw body is read once, then verifyQStashSignature(req, bodyText) validates the Upstash-Signature header against the QStash signing keys. On failure the route returns 401 Unauthorized and does no work.
3

Success

The route does its work, pings its BetterStack heartbeat (pingHeartbeat(...)), and returns 200 with a small JSON body.
4

Failure

On any internal error the route returns a non-2xx (500 Job failed). QStash treats non-2xx as a failure and retries with exponential backoff (up to 3 retries by default), so heavy lifting must be idempotent.
5

Idempotency

Scheduled notification jobs dedupe via a notification log row keyed by recipient + type (+ booking where relevant), so re-runs and QStash retries never double-send.
Not for direct client use. These endpoints are intended to be called only by QStash. Requests without a valid Upstash-Signature are rejected with 401. There is no session/role check and no { success, data } envelope — do not call them from the web app or from API integrations.

Signature verification & local fallback

verifyQStashSignature reads the signing keys directly from the environment so the app builds without them. It never throws — any verification error fails closed (returns false401).

Heartbeats

On success each route calls pingHeartbeat(name), which fetches process.env['BETTER_STACK_HEARTBEAT_' + name]. If that variable is unset the ping is a no-op (the job still succeeds); the helper never throws.
invoice-pdf is the exception — it does not ping a heartbeat. Instead it returns a 200 with an outcome flag (attached, reason, sent) describing whether the PDF was rendered and attached, falling back to a no-attachment invoice email when the Cloud Run render service is unconfigured or errors.

Response shapes

Scheduled jobs that iterate records return a processed count of how many notifications were sent in that run:

Triggered job payloads

The five triggered jobs are enqueued with a JSON body identifying the single record to act on. The body is parsed defensively — a missing or malformed payload yields {}, and the route simply does nothing (still 200) rather than erroring.
Scheduled jobs take no meaningful body — they query the database for the records due in the current run, so the request body is empty (the raw text is still read for signature verification).
Last modified on June 29, 2026