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 arePOST. “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.
- Tab Title
- Tab Title
Invocation contract
Every job route follows the same thin orchestration shape:1
Method
The route accepts onlyPOST.2
Signature verification
The raw body is read once, thenverifyQStashSignature(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 anotification 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 false → 401).
Heartbeats
On success each route callspingHeartbeat(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
- Tab Title
- Tab Title
- Tab Title
- Tab Title
- Tab Title
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.