Why Your Mobile App Needs Background Jobs
When building mobile apps, there's always work that needs to happen behind the scenes — even when users aren't actively using your app. Scheduled data syncs, personalized push notifications, content refreshes from external APIs — these background jobs are what transform a good app into a great one.
Trigger.dev is an open-source background job platform that lets you write jobs in TypeScript. It comes with built-in cron scheduling, webhook triggers, and retry mechanisms, making it a perfect fit for automating server-side processes in Rork-built mobile apps.
Trigger.dev Fundamentals and How It Fits with Rork
Trigger.dev is a Node.js / TypeScript-based job scheduler with several powerful features.
- Cron Jobs: Schedule tasks to run at specific times ("every morning at 9 AM", "every hour")
- Event-Driven Execution: Trigger jobs via webhooks or API calls
- Automatic Retries: Configurable retry logic with exponential backoff
- Dashboard: Monitor execution history, errors, and logs through a web UI
Since Rork apps are built on React Native + Expo and typically use Supabase or Firebase as their backend, Trigger.dev integrates seamlessly. You can add server-side automation without modifying your app's codebase — the jobs interact directly with your backend services.
Setting Up Your Trigger.dev Project
Start by creating a separate project for your background jobs. Keeping it in its own repository is recommended for cleaner separation of concerns.
# Install the Trigger.dev CLI
npm install -g @trigger.dev/cli
# Initialize your project
mkdir my-rork-app-jobs && cd my-rork-app-jobs
npx trigger.dev@latest init
# Install dependencies
npm install @trigger.dev/sdk @supabase/supabase-jsAfter initialization, you'll have a trigger.config.ts file and a src/trigger/ directory. All job definitions go inside src/trigger/.
Project Structure
my-rork-app-jobs/
├── trigger.config.ts # Trigger.dev configuration
├── src/
│ └── trigger/
│ ├── scheduled/ # Cron jobs
│ │ ├── daily-digest.ts
│ │ └── content-sync.ts
│ ├── events/ # Event-driven jobs
│ │ ├── user-signup.ts
│ │ └── purchase-complete.ts
│ └── utils/
│ ├── supabase.ts # Supabase client
│ └── push.ts # Push notification helpers
├── package.json
└── .env # Environment variables
Example 1 — Automatic Content Sync Job
For news or curation apps, you need to periodically fetch content from external APIs and store it in your database. Here's a job that syncs content every hour into Supabase.
// src/trigger/scheduled/content-sync.ts
import { schedules } from "@trigger.dev/sdk/v3";
import { createClient } from "@supabase/supabase-js";
// Initialize Supabase client
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
// Sync content every hour
export const contentSyncJob = schedules.task({
id: "content-sync",
// Run at the top of every hour
cron: "0 * * * *",
run: async () => {
console.log("Starting content sync...");
// Fetch data from external API
const response = await fetch("https://api.example.com/articles", {
headers: {
Authorization: `Bearer ${process.env.CONTENT_API_KEY}`,
},
});
const articles = await response.json();
// Upsert into Supabase (update if exists, insert if new)
const { data, error } = await supabase
.from("articles")
.upsert(
articles.map((article: any) => ({
external_id: article.id,
title: article.title,
body: article.body,
thumbnail_url: article.image,
published_at: article.publishedAt,
updated_at: new Date().toISOString(),
})),
{ onConflict: "external_id" }
);
if (error) {
// Trigger.dev will automatically retry on error
throw new Error(`Supabase upsert failed: ${error.message}`);
}
return {
syncedCount: articles.length,
timestamp: new Date().toISOString(),
};
// Expected output:
// { syncedCount: 25, timestamp: "2026-03-30T05:00:00.000Z" }
},
});With this job running, your Rork app can use Supabase's realtime subscriptions to automatically display new content as it arrives — no app-side polling needed.
Example 2 — Scheduled Push Notification Delivery
This job sends personalized daily digest notifications every morning based on each user's preferences.
// src/trigger/scheduled/daily-digest.ts
import { schedules } from "@trigger.dev/sdk/v3";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
export const dailyDigestJob = schedules.task({
id: "daily-digest-notification",
// Run at 9 AM JST (midnight UTC)
cron: "0 0 * * *",
run: async () => {
// Get users who have notifications enabled
const { data: users } = await supabase
.from("user_preferences")
.select("user_id, push_token, preferred_categories")
.eq("daily_digest_enabled", true)
.not("push_token", "is", null);
if (!users || users.length === 0) {
return { sent: 0, message: "No eligible users" };
}
// Fetch trending articles from the past 24 hours
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const { data: trending } = await supabase
.from("articles")
.select("id, title, category")
.gte("published_at", yesterday.toISOString())
.order("view_count", { ascending: false })
.limit(3);
// Send via Expo Push API
const messages = users.map((user: any) => ({
to: user.push_token,
title: "Your Daily Digest",
body: trending?.[0]?.title || "New articles are available",
data: { articleId: trending?.[0]?.id },
}));
const pushResponse = await fetch(
"https://exp.host/--/api/v2/push/send",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(messages),
}
);
return {
sent: messages.length,
timestamp: new Date().toISOString(),
};
// Expected output:
// { sent: 142, timestamp: "2026-03-30T00:00:00.000Z" }
},
});On the Rork app side, you just need to collect push tokens using expo-notifications and store them in the user_preferences table. Trigger.dev handles the rest. For advanced notification strategies including segmentation and A/B testing, check out the Push Notification Master Plan for Rork Apps.
Example 3 — Event-Driven Jobs (Triggered by User Actions)
Beyond cron jobs, you can also trigger jobs based on user actions like signups or purchases.
// src/trigger/events/user-signup.ts
import { task } from "@trigger.dev/sdk/v3";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
// Welcome sequence triggered on user signup
export const userSignupJob = task({
id: "user-signup-welcome",
// Retry up to 3 times with exponential backoff
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 30000,
},
run: async (payload: { userId: string; email: string }) => {
// 1. Send welcome email
await sendWelcomeEmail(payload.email);
// 2. Generate initial recommendations
const { data: popular } = await supabase
.from("articles")
.select("id")
.order("view_count", { ascending: false })
.limit(5);
await supabase.from("user_recommendations").insert(
(popular || []).map((article: any) => ({
user_id: payload.userId,
article_id: article.id,
reason: "popular",
}))
);
// 3. Log analytics event
await supabase.from("analytics_events").insert({
event_type: "user_onboarded",
user_id: payload.userId,
metadata: { source: "trigger.dev" },
});
return { success: true, userId: payload.userId };
},
});
async function sendWelcomeEmail(email: string) {
// Email sending logic (Resend, SendGrid, etc.)
console.log(`Sending welcome email to: ${email}`);
}To trigger this job from your Rork app, call a Supabase Edge Function that invokes the Trigger.dev API.
// Supabase Edge Function: functions/on-signup/index.ts
import { tasks } from "@trigger.dev/sdk/v3";
Deno.serve(async (req) => {
const { userId, email } = await req.json();
// Kick off the Trigger.dev job
await tasks.trigger("user-signup-welcome", {
userId,
email,
});
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
});Deployment and Monitoring
Deploying Trigger.dev jobs is straightforward.
# Deploy to production
npx trigger.dev@latest deploy
# Local development and testing
npx trigger.dev@latest devOnce deployed, the Trigger.dev dashboard at https://cloud.trigger.dev gives you full visibility into your jobs.
- Execution History: View success/failure status, execution time, and outputs
- Retry Status: Track automatic retries for failed jobs
- Alerts: Configure Slack or Discord notifications for failures
If you're looking to build a comprehensive autonomous operations framework for your app, the Autonomous Operation Architecture for Mobile Apps guide dives deeper into this topic.
Common Errors and Solutions
Timeout Errors
The default timeout in Trigger.dev is 60 seconds. If your external API calls are slow, extend the timeout with maxDuration.
export const longRunningJob = schedules.task({
id: "long-running-sync",
cron: "0 3 * * *",
// Extend timeout to 5 minutes
maxDuration: 300,
run: async () => {
// Long-running process
},
});Missing Environment Variables
Make sure to set SUPABASE_URL, SUPABASE_SERVICE_KEY, and other required variables in the Trigger.dev dashboard under "Environment Variables." Your local .env file doesn't carry over to production.
Excessive Retries
If you're seeing frequent retries, you might be hitting the external API's rate limits. Increase the factor in your retry configuration to space out attempts, or reduce your batch sizes.
Looking back
Trigger.dev is a powerful addition to any Rork-built mobile app, enabling server-side automation that runs independently of your app. From automatic content syncing and personalized push notifications to event-driven onboarding flows, it covers the background processing needs that make apps feel polished and professional.
Since everything is written in TypeScript, the learning curve is minimal for React Native / Expo developers working with Rork. The built-in dashboard provides the monitoring and alerting you need for production confidence. Start with the free plan, implement a simple cron job, and experience how background automation can elevate your app's user experience.