Skip to main content

Background Jobs

The Ever Works Template includes a robust background job system with a pluggable architecture that supports multiple scheduling backends. Jobs run automatically for tasks such as repository synchronization, subscription management, and analytics cache warming.

Architecture Overview

The background job system follows a Strategy pattern with a common BackgroundJobManager interface and three interchangeable implementations:

ComponentFilePurpose
BackgroundJobManagerlib/background-jobs/types.tsInterface contract for all managers
LocalJobManagerlib/background-jobs/local-job-manager.tssetInterval-based scheduling for development
TriggerDevJobManagerlib/background-jobs/trigger-dev-job-manager.tsTrigger.dev SDK v4 integration for production
NoOpJobManagerlib/background-jobs/noop-job-manager.tsSilent no-op for disabled environments
job-factory.tslib/background-jobs/job-factory.tsFactory + singleton creation logic
config.tslib/background-jobs/config.tsScheduling mode resolution
initialize-jobs.tslib/background-jobs/initialize-jobs.tsCentralized job registration

Scheduling Mode Resolution

The system determines which manager to use based on environment configuration, following a strict priority order:

1. Disabled    -- DISABLE_AUTO_SYNC=true  --> NoOpJobManager
2. Trigger.dev -- Fully configured + production --> TriggerDevJobManager
3. Vercel -- Running on Vercel platform --> Vercel Cron (via vercel.json)
4. Local -- Fallback for all other envs --> LocalJobManager

The resolution logic lives in lib/background-jobs/config.ts:

export function getSchedulingMode(): SchedulingMode {
if (disableAutoSync) return 'disabled';
if (shouldUseTriggerDev()) return 'trigger-dev';
if (isVercelEnvironment()) return 'vercel';
return 'local';
}

The BackgroundJobManager Interface

All managers implement the same interface defined in lib/background-jobs/types.ts:

interface BackgroundJobManager {
scheduleJob(id: string, name: string, job: () => void | Promise<void>, interval: number): void;
scheduleCronJob(id: string, name: string, job: () => void | Promise<void>, cronExpression: string): void;
triggerJob(id: string): Promise<void>;
stopJob(id: string): void;
stopAllJobs(): void;
getJobStatus(id: string): JobStatus | undefined;
getAllJobStatuses(): JobStatus[];
getJobMetrics(): JobMetrics;
}

Key Types

type JobStatusType = 'running' | 'completed' | 'failed' | 'scheduled' | 'stopped';

interface JobStatus {
id: string;
name: string;
status: JobStatusType;
lastRun: Date | null;
nextRun: Date | null;
duration: number;
error?: string;
}

interface JobMetrics {
totalExecutions: number;
successfulJobs: number;
failedJobs: number;
averageJobDuration: number;
lastCleanup: Date;
}

Job Factory and Singleton

The factory in lib/background-jobs/job-factory.ts creates the appropriate manager and exposes a singleton:

import { getJobManager } from '@/lib/background-jobs';

const manager = getJobManager();
manager.scheduleJob('my-job', 'My Job', async () => {
// job logic
}, 60_000);

The singleton ensures only one manager instance exists per process. Use resetJobManager() in tests to clear the instance.

LocalJobManager (Development)

The LocalJobManager uses setInterval and setTimeout for scheduling. It provides:

  • Overlap prevention: Skips execution if a previous run of the same job is still in progress.
  • Metrics tracking: Tracks total executions, success/failure counts, and average duration.
  • Cron-to-interval conversion: Converts common cron expressions to millisecond intervals for approximate local scheduling.
  • Quiet development mode: Reduces logging noise when NODE_ENV=development.

Supported cron conversions:

Cron ExpressionInterval
*/30 * * * * *30 seconds
*/2 * * * *2 minutes
*/5 * * * *5 minutes
*/15 * * * *15 minutes
0 * * * *1 hour
0 9 * * *24 hours

TriggerDevJobManager (Production)

The TriggerDevJobManager registers schedules with the Trigger.dev SDK v4. Key behaviors:

  • No local timers: Does not run setInterval -- actual execution is handled by the Trigger.dev worker process.
  • Lazy SDK loading: Dynamically imports @trigger.dev/sdk to prevent bundling issues.
  • Interval-to-cron conversion: Converts millisecond intervals to cron expressions for the Trigger.dev API.
  • Metric recording: Records execution metrics when the worker invokes the run handler.

Configuration

Set the following environment variables to enable Trigger.dev:

TRIGGER_DEV_API_KEY=tr_dev_xxxxx
TRIGGER_DEV_API_URL=https://api.trigger.dev # optional, defaults to this
TRIGGER_DEV_ENABLED=true
TRIGGER_DEV_ENVIRONMENT=production # or staging

The manager only activates when all of these conditions are met:

  1. TRIGGER_DEV_API_KEY and TRIGGER_DEV_API_URL are both set (isFullyConfigured)
  2. TRIGGER_DEV_ENABLED is true
  3. NODE_ENV is production

NoOpJobManager (Disabled)

When DISABLE_AUTO_SYNC=true is set in development, the NoOpJobManager silently ignores all scheduling calls. Every method is a no-op, and metrics remain at zero. This is useful for:

  • Running the dev server without background noise
  • Debugging frontend-only features
  • Reducing resource usage during UI development

Registered Jobs

Jobs are registered centrally in lib/background-jobs/initialize-jobs.ts. This module runs during application startup via the instrumentation hook.

Core Jobs

Job IDNameScheduleDescription
repository-syncRepository SynchronizationEvery 5 minutesSyncs content from the Git-based CMS repository
subscription-renewal-reminderSubscription Renewal ReminderDaily at 9:00 AMSends email reminders for subscriptions expiring in 7 days
subscription-expired-cleanupSubscription Expiration CleanupDaily at midnightProcesses and expires subscriptions past their end date

Analytics Jobs

Registered by AnalyticsBackgroundProcessor in lib/services/analytics-background-processor.ts:

Job IDNameInterval
analytics-user-growthUser Growth Aggregation10 minutes
analytics-activity-trendsActivity Trends Aggregation5 minutes
analytics-top-itemsTop Items Ranking15 minutes
analytics-recent-activityRecent Activity Update2 minutes
analytics-performance-metricsPerformance Metrics Update30 seconds
analytics-cache-cleanupCache Cleanup1 hour

Trigger Task ID Definitions

Task IDs and cron schedules are defined in lib/background-jobs/triggers/:

FileTask IDsPurpose
analytics.tsAnalyticsTaskIdsAnalytics cache warming and cleanup
sync.tsSyncTaskIdsRepository synchronization
subscriptions.tsSubscriptionTaskIdsSubscription lifecycle management
reports.tsReportTaskIdsScheduled report generation

Vercel Cron Integration

When deployed to Vercel, background jobs can also be triggered via Vercel Cron Jobs configured in vercel.json:

{
"crons": [
{ "path": "/api/cron/sync", "schedule": "0 3 * * *" },
{ "path": "/api/cron/subscription-reminders", "schedule": "0 9 * * *" },
{ "path": "/api/cron/subscription-expiration", "schedule": "0 0 * * *" }
]
}

These endpoints hit API routes that execute the same job logic, providing a platform-native scheduling mechanism on Vercel.

Adding a New Background Job

Step 1: Define Task IDs (Optional)

Create or update a file in lib/background-jobs/triggers/:

// lib/background-jobs/triggers/my-feature.ts
export const MyFeatureTaskIds = {
cleanup: 'my-feature-cleanup',
notify: 'my-feature-notify',
} as const;

export const MyFeatureCrons: Record<keyof typeof MyFeatureTaskIds, string> = {
cleanup: '0 2 * * *', // Daily at 2 AM
notify: '*/30 * * * *', // Every 30 minutes
};

Step 2: Implement the Job Function

Create the job logic in lib/services/:

// lib/services/my-feature-jobs.ts
export async function myFeatureCleanupJob(): Promise<void> {
// Your cleanup logic here
console.log('[MyFeature] Running cleanup job...');
}

Step 3: Register in initialize-jobs.ts

Add the job to lib/background-jobs/initialize-jobs.ts:

manager.scheduleCronJob(
'my-feature-cleanup',
'My Feature Cleanup',
async () => {
const { myFeatureCleanupJob } = await import('@/lib/services/my-feature-jobs');
await myFeatureCleanupJob();
},
'0 2 * * *'
);

Important: Use dynamic import() inside the job callback to prevent webpack from bundling Node.js modules during the build phase.

Step 4: Add Vercel Cron (Optional)

If deploying on Vercel, add a cron endpoint to vercel.json and create the corresponding API route:

{ "path": "/api/cron/my-feature-cleanup", "schedule": "0 2 * * *" }

Monitoring and Debugging

Checking Job Status

const manager = getJobManager();
const allStatuses = manager.getAllJobStatuses();
const metrics = manager.getJobMetrics();

console.log('Active jobs:', allStatuses.length);
console.log('Total executions:', metrics.totalExecutions);
console.log('Success rate:', (metrics.successfulJobs / metrics.totalExecutions * 100).toFixed(1) + '%');

Manual Job Triggering

const manager = getJobManager();
await manager.triggerJob('repository-sync');

Disabling Jobs in Development

Set the environment variable to skip all background jobs:

DISABLE_AUTO_SYNC=true

This activates the NoOpJobManager, which silently ignores all scheduling calls.

Best Practices

  1. Always use dynamic imports in job callbacks registered in initialize-jobs.ts to prevent webpack bundling issues.
  2. Keep job functions idempotent -- jobs may run more than once if there are timing overlaps or retries.
  3. Use structured logging with a [JobName] prefix for easier log filtering.
  4. Return result objects from job functions (like JobResult in subscription-jobs.ts) for observability.
  5. Handle errors gracefully -- the manager catches and logs errors, but your job logic should handle partial failures.
  6. Test with the LocalJobManager in development before deploying to Trigger.dev.