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
- Go to the Upstash Console
- Navigate to QStash section
- Copy your credentials:
QSTASH_TOKENQSTASH_CURRENT_SIGNING_KEYQSTASH_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_key3. Enable Cron in Features
Update src/configuration/features.ts:
const local: Features = {
// ... other features
cron: {
provider: "qstash", // or "disabled"
},
};Creating a Schedule in QStash
- Go to the QStash Console
- Click Schedules → Create Schedule
- 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"}
- Destination URL:
Common Cron Expressions
| Expression | Description |
|---|---|
0 9 * * * | Daily at 9:00 AM UTC |
0 */6 * * * | Every 6 hours |
0 9 * * 1 | Every 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:
- Checks for content published within the last 7 days
- Uses cache (if enabled) to track already-notified content
- Sends a single broadcast email to all newsletter subscribers via Resend
- Includes all new content items in a "What's New" digest
Content Detection:
| Cache Enabled | Content Age | Result |
|---|---|---|
| Yes | Not in cache | New (send + cache) |
| Yes | In cache | Skip |
| No | Created today | New (send) |
| No | Created before today | Skip |
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-jobNewsletter 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
| Code | Meaning |
|---|---|
| 200 | Job executed successfully |
| 401 | Invalid webhook signature |
| 404 | Job or provider not found |
| 500 | Internal server error |
| 503 | Cron 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:
- QStash signs each request with your signing key
- The webhook endpoint verifies the signature before executing the job
- 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
- Use HTTPS: Always use HTTPS for webhook endpoints in production
- Rotate Keys: Periodically rotate signing keys using QStash key rotation feature
- Monitor Failures: Set up alerts for job failures in the QStash dashboard
- 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_KEYandQSTASH_NEXT_SIGNING_KEYmatch 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