LogoLaunchSaaS

Scheduled Jobs

Configure scheduled jobs using Upstash QStash for automated server-side workflows.

LaunchSaaS includes a flexible scheduled jobs system powered by Upstash QStash, enabling you to run automated tasks on a schedule without managing your own cron infrastructure.

Updated: 2026-03-22 - Removed the built-in content notification example and clarified custom job registration.

Overview

The scheduled jobs feature provides:

  • Provider Abstraction: Easy to switch between cron providers or disable entirely
  • Webhook-based Execution: Jobs are triggered via HTTP webhooks, perfect for serverless environments
  • Signature Verification: Secure webhook endpoints with automatic signature validation
  • Custom Job Registration: Register any CronJob implementation with your provider

Architecture

┌─────────────────┐     triggers     ┌────────────────────────────────┐
│  Upstash QStash │────────────────►│  POST /api/cron/{provider}/{id} │
│  (Scheduler)    │                  └──────────────┬─────────────────┘
└─────────────────┘                                 │
                                                    │ 1. Check capabilities.cron
                                                    │ 2. Verify webhook signature
                                                    │ 3. Find job in cronProvider.jobs

                                         ┌─────────────────┐
                                         │  Job Handler    │
                                         │  (CronJob)      │
                                         └─────────────────┘

Two sides of the system:

  • CronProvider (capabilities.cron) — external-facing: verifies signatures from QStash, optionally manages schedules via API; also owns the registered job list via cronProvider.jobs

Configuration

1. Get QStash Credentials

  1. Go to the Upstash Console
  2. Navigate to QStash section
  3. Copy your credentials:
    • QSTASH_TOKEN
    • QSTASH_CURRENT_SIGNING_KEY
    • QSTASH_NEXT_SIGNING_KEY

2. Set Environment Variables

Add the following to your .env file:

# Cron Provider - Upstash QStash
QSTASH_TOKEN=your_qstash_token
QSTASH_CURRENT_SIGNING_KEY=your_current_signing_key
QSTASH_NEXT_SIGNING_KEY=your_next_signing_key

3. Enable the Cron Provider in capabilities

Update src/capabilities.ts:

import { QStashCronProvider } from "@launchsaas/cron-qstash";

export const capabilities = {
  // ...
  cron: QStashCronProvider.create([]),
};

When cron is null, the webhook endpoint returns 503 and no external requests are made.

4. Set up the webhook route

Create the API route file that receives QStash webhooks:

// src/app/api/cron/[provider]/[job]/route.ts
import { toHandler } from "@launchsaas/cron";
import { capabilities } from "@/capabilities";

export const { POST } = toHandler(capabilities.cron);

The [provider] segment is validated against capabilities.cron.name and [job] is matched against the registered job IDs.

Creating a Schedule in QStash

  1. Go to the QStash Console
  2. Click SchedulesCreate Schedule
  3. Configure the schedule:
    • Destination URL: https://your-domain.com/api/cron/qstash/my-custom-job
    • Schedule: Use cron expression (e.g., 0 9 * * * for daily at 9 AM UTC)
    • Method: POST

Common Cron Expressions

ExpressionDescription
0 9 * * *Daily at 9:00 AM UTC
0 */6 * * *Every 6 hours
0 9 * * 1Every Monday at 9:00 AM UTC
0 0 1 * *First day of each month

Creating Custom Jobs

1. Create a Job Handler

Create a new file in src/lib/features/cron/jobs/:

// src/lib/features/cron/jobs/my-custom-job.ts
import "server-only";

import type { CronJob } from "@launchsaas/cron";
import { NextResponse } from "next/server";

export class MyCustomJob implements CronJob {
  readonly id = "my-custom-job";
  readonly name = "My Custom Job";
  readonly description = "Does something awesome on a schedule";
  readonly schedule = "0 */12 * * *"; // Informational, actual schedule set in QStash

  async handle(_request: Request): Promise<Response> {
    // Your job logic here

    return NextResponse.json({ success: true, message: "Job completed" });
  }
}

2. Register the Job

Pass it to the provider in src/capabilities.ts:

import { QStashCronProvider } from "@launchsaas/cron-qstash";
import { MyCustomJob } from "@/lib/features/cron/jobs/my-custom-job";

cron: QStashCronProvider.create([
  new MyCustomJob(),
]),

3. Create Schedule in QStash

Add a new schedule in the QStash dashboard pointing to:

https://your-domain.com/api/cron/qstash/my-custom-job

The {job} URL segment must match your job's id property exactly.

How CronProvider Works

When QStash fires a webhook:

  1. POST /api/cron/qstash/my-custom-job arrives
  2. Route checks capabilities.cron — if null, returns 503
  3. Route validates the provider name in the URL against capabilities.cron.name
  4. Route calls capabilities.cron.verifySignature(request) — QStash signature is verified
  5. Route finds the job via capabilities.cron.jobs.find(j => j.id === jobId)
  6. Route calls job.handle(request) — executes your logic
// Simplified route handler
const cronProvider = capabilities.cron; // CronProvider (QStash)
const job = cronProvider.jobs.find((j) => j.id === jobId); // Your job class
await cronProvider.verifySignature(request); // Verify it's really from QStash
await job.handle(request); // Run your code

Newsletter Integration

Custom jobs can integrate with the newsletter system to send broadcast emails:

const newsletter = capabilities.newsletter;
if (newsletter) {
  await newsletter.broadcast({
    subject: "Product update from LaunchSaaS",
    react: MyEmailTemplate({}),
  });
}

Monitoring

QStash Dashboard

Monitor your scheduled jobs in the QStash Console:

  • Schedules: View and manage active schedules
  • Logs: See execution history and errors
  • Messages: Track individual job invocations

Response Codes

CodeMeaning
200Job executed successfully
401Invalid webhook signature
404Job or provider not found
500Internal server error
503Cron is not configured

Security

Signature Verification

All incoming webhook requests are verified using QStash's signature mechanism:

  1. QStash signs each request with your signing key
  2. The webhook endpoint verifies the signature before executing the job
  3. Invalid signatures are rejected with 401 Unauthorized

Never expose your QSTASH_TOKEN or signing keys in client-side code. These are server-only credentials.

Best Practices

  1. Use HTTPS: Always use HTTPS for webhook endpoints in production
  2. Rotate Keys: Periodically rotate signing keys using QStash key rotation feature
  3. Monitor Failures: Set up alerts for job failures in the QStash dashboard
  4. Idempotent Jobs: Design jobs to be idempotent (safe to run multiple times)

Troubleshooting

Common Issues

Job not triggering:

  • Verify the schedule is active in QStash dashboard
  • Check the destination URL is correct and publicly accessible
  • Ensure environment variables are set in production

Signature verification failing:

  • Confirm QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY match QStash console
  • Check for key rotation - update both keys if recently rotated

Job not found (404):

  • Ensure the job ID in the URL matches the id property of the job class exactly
  • Confirm the job instance is passed to QStashCronProvider.create([...]) in src/capabilities.ts

Emails not sending:

  • Verify the newsletter capability is configured (non-null) in src/capabilities.ts
  • Check Resend API key and audience ID are set
  • Look for errors in the job response logs

Content not detected as new:

  • With cache: Check if content was already processed (cached)
  • Without cache: Content must be published TODAY to be detected