> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theroyalglow.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Background Jobs

> The 19 scheduled and event-driven jobs that keep Royal Glow running — 14 QStash scheduled, 4 QStash triggered, 1 GitHub Actions cron.

# Background Jobs

<Info>
  **19 jobs total — 14 QStash scheduled + 4 QStash triggered + 1 GitHub Actions cron.**
  All scheduled and event-driven jobs run on QStash (HTTP → Next.js API routes
  hosted in `apps/admin`, served from `admin.theroyalglow.in/api/jobs/*`). The
  single exception is the `pprd` branch reset (Job 5), a Neon control-plane op on
  GitHub Actions cron. Every job pings a BetterStack heartbeat on success.
</Info>

Royal Glow runs **19 background jobs**. **All scheduled and event-driven jobs run
on QStash** (HTTP → Next.js API routes) — including the pure-SQL maintenance jobs,
which execute their idempotent SQL via `@rgss/db` query functions. The single
exception is the `pprd` branch reset (Job 5), which is a Neon control-plane op and
runs on GitHub Actions cron.

<Warning>
  **pg\_cron is not used.** The 6 DB-maintenance jobs were originally planned on
  pg\_cron, but the free-tier prod Neon compute scales to zero after \~5 min idle
  and pg\_cron only fires while the compute is awake — the late-night windows
  (11:30 PM–2:30 AM IST) would silently never run. QStash POSTs an endpoint that
  *wakes* the compute, so the jobs run reliably at ₹0. The job SQL was ported
  verbatim into `packages/db/src/queries/jobs.ts`.
</Warning>

## Where jobs run

<Tabs>
  <Tab title="QStash">
    Upstash HTTP queue → Next.js API routes in `apps/admin`. Use for **ALL** scheduled
    and triggered jobs (DB maintenance + external services).

    Every QStash job calls a `POST /api/jobs/...` route and is auto-retried (3x,
    exponential backoff) on a non-2xx response. Every job pings a BetterStack
    heartbeat on success so silent failures get detected. The `/api/jobs/*` routes are
    served from `admin.theroyalglow.in/api/jobs/*`; QStash schedules are registered
    from the admin deploy.
  </Tab>

  <Tab title="GitHub Actions cron">
    GitHub CI. Control-plane **only** — Neon `pprd` branch reset + PII strip (Job 5).
    A Neon Branch Reset API call cannot run inside a normal request, so it lives in CI.
  </Tab>
</Tabs>

<Info>
  All schedules are in UTC; the salon operates IST (UTC+5:30).
</Info>

## Not jobs (synchronous / external)

Three time-critical actions deliberately run **in-request**, not as jobs:

* **Invoice email + PDF** — fired immediately when the receptionist marks payment received.
* **Gems-earned push** — fired in the same request as invoice completion.
* **Re-engagement email** — handled inside Brevo automations (marketing), not counted here.

## Job inventory

<Tabs>
  <Tab title="Scheduled — DB maintenance (7)">
    These run in the early IST hours when traffic is zero. **Jobs 1–4, 6 and 7 run on
    QStash** (each POSTs its `/api/jobs/...` route, which runs the idempotent SQL via
    a `@rgss/db` query function) — *not* pg\_cron. Job 5 runs via GitHub Actions cron
    (Neon branch reset) plus SQL anonymisation.

    <AccordionGroup>
      <Accordion title="Job 1 — Nightly sales summary · 0 18 * * * UTC · 11:30 PM IST">
        Aggregate the day's invoices into `daily_sales_summary`. Runs on QStash.
      </Accordion>

      <Accordion title="Job 2 — Membership auto-expire · 30 18 * * * UTC · 12:00 AM IST">
        Flip `spa_membership` from `active` to `expired` past `expires_at`. Runs on QStash.
      </Accordion>

      <Accordion title="Job 3 — Offer auto-expire · 35 18 * * * UTC · 12:05 AM IST">
        Deactivate offers whose `end_date` has passed. Runs on QStash.
      </Accordion>

      <Accordion title="Job 7 — Gems auto-expire · 40 18 * * * UTC · 12:10 AM IST">
        Deduct gems 365 days after they were earned. Runs on QStash.
      </Accordion>

      <Accordion title="Job 6 — Monthly GST summary · 30 19 1 * * UTC · 1:00 AM IST (1st)">
        Aggregate the prior month into `monthly_gst_summary` (SAC 999721). Runs on QStash.
      </Accordion>

      <Accordion title="Job 5 — pprd DB sync · 30 19 * * * UTC · 1:00 AM IST">
        GitHub Actions resets `pprd` from `prod`, then strips PII. Control-plane op.
      </Accordion>

      <Accordion title="Job 4 — Session cleanup · 0 21 * * 0 UTC · 2:30 AM IST Sunday">
        Delete expired Better Auth `session` rows. Runs on QStash.
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="Scheduled — application (8)">
    HTTP-triggered application jobs on a fixed cadence (the 6 DB-maintenance jobs are
    also QStash-scheduled, for 14 QStash-scheduled jobs in total).

    <AccordionGroup>
      <Accordion title="Job 8 — Appointment reminders · every 15 min (8 AM–10 PM IST)">
        Endpoint `/api/jobs/appointment-reminders`. External calls: web-push + Resend.
      </Accordion>

      <Accordion title="Job 9 — Membership expiry alerts · daily 12:30 AM IST">
        Endpoint `/api/jobs/membership-expiry`. External calls: web-push + Resend.
      </Accordion>

      <Accordion title="Job 10 — Birthday emails · daily 9:30 AM IST">
        Endpoint `/api/jobs/birthday-emails`. External calls: Brevo + web-push.
      </Accordion>

      <Accordion title="Job 11 — Membership usage nudges · daily randomized batch">
        Endpoint `/api/jobs/membership-usage-nudges`. External calls: web-push + Resend.
      </Accordion>

      <Accordion title="Job 12 — Lead follow-up reminders · daily 10:30 AM IST">
        Endpoint `/api/jobs/lead-followups`. External calls: web-push.
      </Accordion>

      <Accordion title="Job 13 — Daily sales report · daily 10:30 PM IST">
        Endpoint `/api/jobs/daily-sales-report`. External calls: Slack + Resend.
      </Accordion>

      <Accordion title="Job 14 — Weekly summary report · Mon 9:00 AM IST">
        Endpoint `/api/jobs/weekly-report`. External calls: Slack + Resend.
      </Accordion>

      <Accordion title="Job 15 — Gems expiry reminder · daily 10:30 AM IST">
        Endpoint `/api/jobs/gems-expiry-reminder`. External calls: web-push only.
      </Accordion>
    </AccordionGroup>

    Reminders fire only when the customer has the relevant preference enabled
    (`appointment_reminders_enabled`, `membership_alerts_enabled`) or
    `marketing_consent` for birthday/marketing sends. Each job writes a
    `notification` row for idempotency so the same alert is never sent twice.
  </Tab>

  <Tab title="Triggered (4)">
    Event-driven — enqueued with a delay by an API route when a business event
    occurs, not on a fixed schedule.

    <AccordionGroup>
      <Accordion title="Job 16 — Post-service follow-up · trigger booking.status → completed · +24h">
        Endpoint `/api/jobs/post-service-followup`. External calls: Brevo.
      </Accordion>

      <Accordion title="Job 17 — Stale pending booking alert · trigger booking created pending · +2h">
        Endpoint `/api/jobs/stale-booking-alert`. External calls: web-push. Escalates to an
        auto-reject (with apology to the customer) if a booking is still pending after 24 hours.
      </Accordion>

      <Accordion title="Job 18 — No-show check · trigger booking end_time reached · +15min">
        Endpoint `/api/jobs/noshow-check`. External calls: web-push. Never auto-marks a
        no-show — a receptionist may have extended the session without updating the system,
        so it always alerts a human to confirm.
      </Accordion>

      <Accordion title="Job 19 — Membership expired notice · trigger spa_membership.expires_at passes · +1h">
        Endpoint `/api/jobs/membership-expired-notice`. External calls: Resend.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>

## Related pages

<Columns cols={2}>
  <Card title="Jobs API" href="/docs/api-reference/jobs">
    The QStash-triggered /api/jobs/\* route contracts
  </Card>

  <Card title="Data Model" href="/docs/data-model">
    The tables these jobs scan and update
  </Card>

  <Card title="Deployment" href="/docs/deployment">
    How jobs are monitored via BetterStack heartbeats
  </Card>
</Columns>


## Related topics

- [Background Jobs](/content/docs/api-reference/jobs.md)
- [Pages & Routes](/content/docs/pages-and-routes.md)
- [Low-Level Design](/content/docs/system-design/low-level-design.md)
- [Notifications & Email](/content/docs/notifications-email.md)
- [Data Model](/content/docs/data-model.md)
