Timezone Aware Events with Taskless

Imagine, for a moment, you're building a mobile-first professional networking app. One of its most important features is the "Daily Briefing" where you tell your user about their upcoming day, sent at 8:30 every morning. Because our users are business professionals, they're likely to be traveling and changing time zones frequently.

So, when is 8:30 am for that user? With Taskless, you can make your events timezone aware, and that Daily Briefing arrives exactly when it should.

A completed example is available in the examples folder if you'd like to just look at the final product.

Our Core Event & API

First, it's helpful to be familiar with how jobs are enqueued in Taskless. This is because when a job in Taskless is enqueued with the same Job Identifier, it automatically updates the matching job if it exists. In database terms, this is an upsert.

All jobs scheduled in Taskless are treated like an upsert

To keep our data payload as small as possible, we're only going to deal with the userId we're scheduling a briefing for. In our API endpoint, we would add any additional security checks before we agree to enqueue the job. Our job doesn't do much, but it's easy to see where we'd add all our backend service calls and email generation work. We'll create this queue inside of our Next API folder at /api/queues/briefing.ts.

1// api/queues/briefing.ts
2
3import { createQueue } from "@taskless/next";
4
5interface DailyBriefing {
6 userId: string;
7}
8
9export default createQueue<DailyBriefing>(
10 "daily-briefing",
11 "/api/queues/briefing",
12 async (job, api) => {
13 console.info(`Daily briefing for ${job.userId}`);
14 return { ok: true };
15 }
16);

We'll also want our API that our mobile app calls, used for setting the timezone. For now, we'll keep that information as placeholders. Our REST-like endpoint will be at /api/update-briefing.ts.

1// api/update-briefing.ts
2
3import { sleep } from "@/util/sleep";
4import type { NextApiRequest, NextApiResponse } from "next";
5
6type Data = {
7 ok: boolean;
8};
9
10const validateSession = () => sleep(80, "Verify user is valid");
11
12const handler = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
13 await validateSession();
14 res.status(200).json({ ok: true });
15};
16
17export default handler;

Mobile: Capturing Timezone

To update our briefing, we'll want to call our API with information about the current user's timezone as an IANA identifier. Mobile devices make this available via various APIs, and we can use these timezone IDs for scheduling our briefing to the correct timezone for the user.

One of the best parts about using IANA identifiers is that the tz database knows what these IDs mean, if they include daylight saving time, and their offsets (❤️ you Chatham Islands, New Zealand and your UTC +12:45).

1import DeviceInfo from "react-native-device-info";
2
3console.log(DeviceInfo.getTimezone()); // 'America/New_York'

No matter how you get it, we'll make a JSON request to our /api/update-briefing endpoint with a payload that tells us about the possibly new timezone. For brevity, we'll assume you're using sessions, JWTs, or another means to secure your API endpoint.

1{
2 "userId": "03952ed3-d7be-4e26-874a-15052735ee9f",
3 "tz": "America/New_York"
4}

Upserting Into Taskless

When our API receives a valid request, we can call enqueue() into Taskless with the new timezone information. To make sure our first run is correct, we'll use the excellent luxon library to set the first run. Once we add the timezone, Taskless can take care of every run after that.

1import type { NextApiRequest, NextApiResponse } from "next";
2import { DateTime } from "luxon";
3import { sleep } from "@/util/sleep";
4import briefing from "./queues/briefing";
5
6type Data = {
7 ok: boolean;
8};
9
10const validateSession = () => sleep(80, "Verify user is valid");
11
12const getSession = async () => {
13 // these values should come from your session
14 return {
15 userId: "03952ed3-d7be-4e26-874a-15052735ee9f",
16 };
17};
18
19const handler = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
20 await validateSession();
21 const { userId } = await getSession();
22
23 const timezone = `${req.body.timezone ?? "UTC"}`;
24 const now = DateTime.now().setZone(timezone);
25
26 briefing.enqueue(
27 `${userId}`,
28 {
29 userId,
30 },
31 {
32 runAt: (now < now.set({ hour: 8, minute: 0, second: 0, millisecond: 0 })
33 ? now.startOf("day").set({ hour: 8 })
34 : now.startOf("day").plus({ days: 1 }).set({ hour: 8 })
35 ).toISO(),
36 runEvery: "P1D",
37 timezone,
38 }
39 );
40
41 res.status(200).json({ ok: true });
42};
43
44export default handler;

What's Next

Timezone aware jobs are just one of the many things you can do with Taskless. Check out the getting started doc for other guides and ideas.