LogoLaunchSaaS

Scheduled Jobs (Cron)

Configure scheduled jobs using Upstash QStash for automated tasks like newsletter notifications.

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.

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
  • Built-in Content Notification Job: Automatically notify newsletter subscribers about new blog posts and changelog entries

Architecture

┌─────────────────┐     triggers     ┌─────────────────┐
│  Upstash QStash │────────────────►│  Webhook API    │
│  (Scheduler)    │                  │  /api/cron/...  │
└─────────────────┘                  └────────┬────────┘


                                     ┌─────────────────┐
                                     │  Job Handler    │
                                     │  (Your Logic)   │
                                     └────────┬────────┘


                                     ┌─────────────────┐
                                     │  Newsletter     │
                                     │  .broadcast()   │
                                     └─────────────────┘

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 Cron in Features

Update src/configuration/features.ts:

const local: Features = {
  // ... other features
  cron: {
    provider: "qstash", // or "disabled"
  },
};

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/content-notification
    • Schedule: Use cron expression (e.g., 0 9 * * * for daily at 9 AM UTC)
    • Method: POST
    • Body: {"trigger": "scheduled"}

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

Built-in Jobs

Content Notification Job

Automatically sends email notifications to newsletter subscribers when new blog posts or changelog entries are published.

Job ID: content-notification

Endpoint: /api/cron/qstash/content-notification

How it works:

  1. Checks for content published within the last 7 days
  2. Uses cache (if enabled) to track already-notified content
  3. Sends a single broadcast email to all newsletter subscribers via Resend
  4. Includes all new content items in a "What's New" digest

Content Detection:

Cache EnabledContent AgeResult
YesNot in cacheNew (send + cache)
YesIn cacheSkip
NoCreated todayNew (send)
NoCreated before todaySkip

When cache is disabled, only content created today will be included to prevent re-sending for older content.

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 { NextResponse } from "next/server";
import { registerJob } from "./index";

registerJob({
  id: "my-custom-job",
  name: "My Custom Job",
  description: "Does something awesome on a schedule",
  schedule: "0 */12 * * *", // Informational, actual schedule set in QStash
  handler: async (request: Request) => {
    // Your job logic here

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

2. Register the Job

Import your job in the webhook route file to ensure it's registered:

// src/app/api/cron/[provider]/[job]/route.ts
import "@/lib/features/cron/jobs/my-custom-job";

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

Newsletter Integration

The scheduled jobs feature integrates with the newsletter system to send broadcast emails:

Newsletter Subscription

Visitors can subscribe to the newsletter via forms on:

  • Blog listing page
  • Changelog page

Broadcast Emails

The Newsletter.broadcast() method uses Resend's Broadcast API to efficiently send emails to all subscribers in a single API call, avoiding timeout issues with large lists.

await Newsletter.broadcast({
  subject: "What's New: 3 updates from LaunchSaaS",
  react: ContentNotificationTemplate({
    blogs: [...],
    changelogs: [...],
    baseUrl: "https://your-domain.com",
  }),
});

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 disabled

Disabling Scheduled Jobs

To disable the cron feature entirely, update your configuration:

// src/configuration/features.ts
cron: {
  provider: "disabled",
},

When disabled:

  • The webhook endpoint returns 503
  • Job handlers become no-ops
  • No external requests are made

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

Emails not sending:

  • Verify newsletter provider is configured (features.newsletter.provider)
  • 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