Build a Time Zone Meeting API

Build a Cloudflare Worker API that converts meeting times across time zones.

Introduction

30 Second Summary

A meeting invite that looks perfect in one city can land an hour off somewhere else after the clocks change. Moving that meeting to next month creates another chance for the calendar to surprise you.

In this project, you will build a public meeting-time API with Temporal on a Cloudflare Worker. It returns each attendee's local time plus the same meeting one month later.

What You'll Build

Open a public URL to see one meeting displayed at the correct local time for every attendee.

By the end of this project, you'll have:

  • A public URL that accepts a meeting time and returns a JSON list for at least three time zones.
  • A recurring meeting result that moves the date one month forward correctly across a month boundary.
  • A daylight saving comparison that shows the local time from Temporal.ZonedDateTime beside a plain Date calculation.
  • Secret Mission: An optional challenge to push your skills further.

Are there any prerequisites?

You need Node.js and a free Cloudflare account before you begin. The Workers free plan covers the whole project.

Before We Start

Your meeting API eventually runs on Cloudflare Workers. First, you need an account that can receive the deployment.

Local development depends on Node.js. Its bundled npm command gives you access to Cloudflare's project tools later.

In this step, get ready to:
  • Prepare a Cloudflare account for the future deployment.
  • Install Node.js with its bundled npm package manager.
  • Confirm your browser can reach Cloudflare.
Set up your Cloudflare account

Cloudflare uses your account to manage deployed Workers. This project starts locally before your account receives the finished API.

  • Choose the tab that matches your current Cloudflare account status.

✔️ I can access the dashboard

Your existing account already covers this prerequisite. Confirm that you can still reach it before moving on.

You'll see the Cloudflare dashboard after signing in. This confirms your browser can reach the account you use for deployment.

ⓧ I need a Cloudflare account

Your password stays inside Cloudflare's sign-up flow. Keep it out of screenshots. Keep it out of project files.

  • Open the Cloudflare Workers sign-up page in your browser.
  • Enter your email address in the Email field.
  • Enter a password in the Password field.
  • Select Create Account.
  • Open the verification email Cloudflare sends.
  • Select the verification link in that email.
  • Return to the Cloudflare dashboard in your browser.

You'll see the Cloudflare dashboard after verification. Your account is now available for the Worker deployment later in the project.

Can't Reach the Dashboard?

Check that you verified the same email address used during sign-up. If you already had an account, use its existing sign-in details.

If the page still does not load, try the link in a private browser window. This rules out an expired browser session.

Help me access my Cloudflare account.

Check Node.js and npm

Node.js runs Cloudflare's project tooling on your computer. The npm command arrives with Node.js through the official installer.

  • Switch to your terminal.
  • Check both local tools by running these commands:
node --version
npm --version

What Do These Checks Prove?

  • The node command reports the installed Node.js version.
  • The npm command reports the installed package manager version.
  • The --version flag checks each tool without changing your computer.
  • Choose the tab that matches your terminal output.

✔️ I see two version numbers

Both commands reached their installed tools. That's your local toolchain ready for the Cloudflare project creator.

ⓧ A command is not found

The official Node.js installer includes npm. Installing Node.js supplies both tools in one setup flow.

  • Visit the official Node.js download page in your browser.
  • Select the current LTS release for your operating system.
  • Run the downloaded installer.
  • Accept the installer's default options.
  • Close your current terminal window.
  • Open your terminal again from your computer's app search.
  • Check the installation by running these commands:
node --version
npm --version

Why Use a Fresh Terminal?

A fresh terminal reloads the locations where your computer searches for commands. This lets it find the newly installed Node.js tools.

The first line checks Node.js. The second line checks the npm installation bundled with it.

You'll see a version number on each line. This confirms that the installation reached your terminal.

Still Missing a Command?

Make sure the installer completed before reopening your terminal. Restart your computer if a fresh terminal still cannot find the tools.

Help me fix my Node.js installation.

Confirm your setup

A final check proves that both sides of your setup are ready. Your browser reaches Cloudflare while your terminal reaches the local development tools.

  • Refresh the Cloudflare dashboard in your browser.

You'll see the dashboard load again. This confirms that your Cloudflare session is active.

Before you run the final check, consider whether both commands can print a version number.

  • Confirm both local tools are available by running these commands:
node --version
npm --version

What Does the Final Check Prove?

Two version numbers show that your terminal can find Node.js plus npm. You can now run the Cloudflare project tooling used in the next step.

You'll see one version number for Node.js. You'll see another version number for npm.

Only See One Version Number?

Reinstall Node.js from the official download page because its installer includes npm. Reopen your terminal after the installation finishes.

Help me diagnose my version check.

Your setup is ready. Next, you'll authenticate with Cloudflare to create your Worker project.

Create and Run Your Worker

Time-zone logic needs a reliable place to run before you can trust its answers. A local baseline proves the Worker can answer a request before date math enters the picture.

In this step, you'll use the Cloudflare CLI to scaffold a Cloudflare Worker.

Wrangler connects the project to your account. It also serves the untouched Worker on your machine.

In this step, get ready to:
  • Generate the time-zone-meeting-api Worker project.
  • Authorize Wrangler to access your Cloudflare account.
  • Start the Worker locally and view its default response.
Create the Worker project

The scaffold generator creates the folder structure for your Worker. It also installs Wrangler inside the project.

  • Move to your Desktop by running this command:
cd ~/Desktop

Why Start on Your Desktop?

This location keeps the new time-zone-meeting-api folder easy to find. Every project file lands inside that folder.

The next command opens an interactive setup wizard. Keep these selections nearby before you start.

  • Approve the npm package installation prompt if it appears.
  • Choose Hello World example when the wizard asks what you want to start with.
  • Choose Worker only when the wizard asks for a template.
  • Choose JavaScript when the wizard asks for a language.
  • Choose Yes when the wizard asks about Git version control.
  • Choose Yes when the wizard asks about an AGENTS.md file.
  • Choose No when the wizard asks whether to deploy the application.
  • Generate the Worker scaffold by running this command:
npm create cloudflare@latest -- time-zone-meeting-api

What Does This Command Do?

  • The project name creates a folder called time-zone-meeting-api.
  • The @latest tag asks npm for the current Cloudflare project generator.
  • The generator creates the Worker source file and its configuration.

The setup finishes after the project dependencies are installed. You should see confirmation that the time-zone-meeting-api application was created.

Scaffold Not Finishing?

Confirm that your terminal still has internet access. Check whether the wizard is waiting for an unanswered prompt.

Ask for help with the Worker scaffold. You can also share the non-sensitive terminal output in the NextWork community.

  • Enter the generated time-zone-meeting-api folder by running this command:
cd time-zone-meeting-api

What Changed?

Your terminal now points at the generated Worker project. Commands run here can use the project's installed copy of Wrangler.

✔️ Awesome, I've got everything!

Your terminal is inside time-zone-meeting-api. The generated project contains the expected Worker scaffold.

ⓧ I'd like to double check the full code

The generated scaffold matches this project map.

  • The time-zone-meeting-api/ folder contains the Cloudflare Worker project scaffold created with Wrangler.
  • The time-zone-meeting-api/package.json file contains the project package manifest generated by the Cloudflare CLI.
  • The time-zone-meeting-api/src/index.js file is the default Cloudflare Worker source file that returns the scaffold response.
  • The time-zone-meeting-api/wrangler.jsonc file contains the Worker configuration generated by the Cloudflare CLI for the time-zone-meeting-api Worker.
Authenticate Wrangler

Wrangler uses OAuth to connect the CLI to your Cloudflare account. You do not need to paste an API token into the terminal.

  • Start the Wrangler authorization flow by running this command:
npx wrangler login

How Does Wrangler Authentication Work?

Wrangler opens a Cloudflare authorization page in your default browser. The terminal waits while you approve the request.

  • Complete the Cloudflare account sign-in in your browser.
  • Approve Wrangler's requested access.
  • Return to the terminal after the browser confirms authorization.

Before you check, predict which account detail would prove that Wrangler is connected.

  • Confirm the authenticated account by running this command:
npx wrangler whoami

What Does This Command Check?

Wrangler retrieves information for the authenticated user. A successful response proves that the CLI can access your Cloudflare account.

You should see your authenticated user information in the terminal. Your Cloudflare identity is now connected to this project.

Browser Authorization Not Completing?

  • Copy the authorization URL from the terminal into your browser if no tab opens.
  • Restart the authorization flow if the original request expires.

Ask for help with Wrangler authentication. You can also describe the non-sensitive result in the NextWork community.

Run the Worker locally

A local server lets your browser send a request to the Worker before deployment. Wrangler keeps the current terminal occupied while that server is running.

Before you run it, predict whether the untouched scaffold can already return a browser response.

  • Start the local Worker server by running this command:
npx wrangler dev

What Does This Command Do?

Wrangler starts a local development server for the Worker in src/index.js. Requests to the local address execute that source file.

The terminal prints the local development address. It then waits for browser requests.

You should see Hello World! in the browser. That response proves the default Worker is running locally.

Local Worker Not Loading?

  • Keep the Wrangler development process running in the terminal.
  • Use the local address printed by your terminal if it differs from the expected address.
  • Check the terminal for a project configuration or port conflict.

Ask for help with the local Worker. You can also share the non-sensitive terminal output in the NextWork community.

That's your first Worker response running from your own machine. Next, you'll prepare this runtime for time-zone date logic.

Enable Temporal Support

Cloudflare Workers protects runtime changes behind compatibility flags. Temporal stays unavailable to this project until its flag is enabled.

Your local Wrangler process already proves that the Worker can handle requests. In this step, you will enable Temporal support before logging a current time value from the runtime.

In this step, get ready to:
  • Enable Temporal through the Worker compatibility settings.
  • Log the current instant from the Worker request handler.
  • Trigger a request to confirm the current value in the local terminal.
Enable the Temporal flag

A compatibility flag opts one Worker into a runtime feature before that feature becomes the default. The setting belongs in wrangler.jsonc because this file controls the runtime used for local development.

  • In the time-zone-meeting-api project from earlier, select wrangler.jsonc in your code editor's file sidebar.
  • Find the existing compatibility_date property.
  • Add this property directly below compatibility_date:
"compatibility_flags": ["temporal"],

What does this setting do?

  • The compatibility_flags property lists runtime features that this Worker uses.
  • The temporal value exposes the Temporal global inside the Worker runtime.
  • Keeping the setting in wrangler.jsonc gives local development the same Temporal capability that the deployed Worker uses.
  • Save wrangler.jsonc.
  • Return to the Wrangler terminal from earlier.

Wrangler reloads the Worker configuration. Your local development session remains available at the same URL.

Wrangler rejects the configuration?

Check that compatibility_flags sits at the top level of the configuration object. Make sure the surrounding properties are separated with commas.

Ask for help checking the configuration structure.

Log the current Temporal value

The fetch() handler runs whenever the browser requests your Worker. Logging inside that handler gives you immediate proof that the runtime recognizes Temporal.

  • Select src/index.js in your code editor's file sidebar.
  • Find the existing return statement inside fetch().
  • Add this code directly above the return statement:
// Log the current instant to confirm Temporal is available.
console.log(Temporal.Now.instant().toString());

What does this code do?

  • Temporal.Now.instant() asks the runtime for the current point in time.
  • toString() turns the instant into a readable timestamp.
  • console.log() sends that timestamp to the local Wrangler terminal.
  • Save src/index.js.

Before you refresh, do you expect the browser response to change or the terminal to reveal the new result?

  • Return to the local development URL from earlier.
  • Refresh the browser page.
  • Switch back to the Wrangler terminal.

You will still see the scaffold response in the browser. The Wrangler terminal now shows a current timestamp from Temporal.

No timestamp in the terminal?

Confirm that you refreshed the local development URL after saving src/index.js. Each browser request runs the logging line again.

Check that the logging line sits inside fetch() before the return statement. Code placed after the return statement never runs.

Ask for help tracing the missing Temporal log.

✔️ Awesome, I've got everything!

Your saved Worker source now logs the current Temporal instant before returning its response.

ⓧ I'd like to double check the full code

Compare your src/index.js file with this completed version.

export default {
	async fetch(request, env, ctx) {
		// Log the current instant to confirm Temporal is available.
		console.log(Temporal.Now.instant().toString());

		return new Response("Hello World!");
	},
};

That is the runtime gate cleared: your Worker can now read Temporal values during local requests. Next, you will turn that capability into time zone conversions for meeting attendees.

Convert Meeting Times

Your project already runs on Cloudflare Workers with Temporal enabled. The current-time log proved that the runtime can use the API.

A meeting time only becomes useful when every attendee can read it in their own city. This step attaches the source clock reading to an IANA time zone before converting the same instant for each attendee.

In this step, get ready to:
  • Read the meeting details from URL query parameters.
  • Represent the meeting with Temporal.ZonedDateTime.
  • Return each attendee's local time as JSON.
Build the conversion endpoint

A ZonedDateTime stores the local date with its time zone. The bracketed zone in the request tells Temporal which location owns the original clock reading.

  • In the Explorer sidebar from earlier, select src/index.js.
  • Replace the contents of src/index.js by pasting the code below:
export default {
  async fetch(request) {
    // Read the meeting details from the request URL
    const url = new URL(request.url);
    const meetingTime = url.searchParams.get("meetingTime");
    const timeZoneList = url.searchParams.get("timeZones");

    // Explain which inputs the endpoint requires
    if (!meetingTime || !timeZoneList) {
      return Response.json({ error: "Add meetingTime and timeZones query parameters." }, { status: 400 });
    }

    // Preserve the meeting instant while changing its displayed zone
    const meeting = Temporal.ZonedDateTime.from(meetingTime);
    const localTimes = timeZoneList.split(",").map((timeZone) => {
      const zone = timeZone.trim();
      const localMeeting = meeting.withTimeZone(zone);

      return {
        timeZone: zone,
        localTime: localMeeting.toString(),
      };
    });

    return Response.json(localTimes);
  },
};

What does this code do?

  • The URL object gives the handler access to the request's query parameters.
  • The meetingTime value carries the source date with its bracketed time zone.
  • The timeZoneList value contains the comma-separated attendee zones.
  • The withTimeZone() method creates an attendee-local view of the same meeting instant.
  • The Response.json() method sends the completed list back to the browser.
  • Save src/index.js.
  • Return to the local Wrangler terminal from earlier.
  • Confirm that the Worker reloads without reporting a syntax problem.

Seeing a reload problem?

Compare the braces around the exported object with the reference below. Check that every string has matching quotation marks.

Ask for help with the exact code in your file: Why did my Worker stop reloading after I replaced src/index.js?

✔️ Awesome, I've got everything!

Your conversion endpoint is saved. Keep the local Worker running for the browser test.

ⓧ I'd like to double check the full code

Compare your complete src/index.js file with this reference.

export default {
  async fetch(request) {
    // Read the meeting details from the request URL
    const url = new URL(request.url);
    const meetingTime = url.searchParams.get("meetingTime");
    const timeZoneList = url.searchParams.get("timeZones");

    // Explain which inputs the endpoint requires
    if (!meetingTime || !timeZoneList) {
      return Response.json({ error: "Add meetingTime and timeZones query parameters." }, { status: 400 });
    }

    // Preserve the meeting instant while changing its displayed zone
    const meeting = Temporal.ZonedDateTime.from(meetingTime);
    const localTimes = timeZoneList.split(",").map((timeZone) => {
      const zone = timeZone.trim();
      const localMeeting = meeting.withTimeZone(zone);

      return {
        timeZone: zone,
        localTime: localMeeting.toString(),
      };
    });

    return Response.json(localTimes);
  },
};
Send a three-city request

Query parameters let one endpoint process different meeting details without editing code. The meetingTime value includes the bracketed source zone.

The timeZones value contains the comma-separated attendee destinations.

Before you send the request, do you expect the three cities to display the same clock time?

  • Switch back to the browser tab showing your local Worker.
  • Replace everything after the local Worker address with ?meetingTime=2026-12-15T09:30[America/New_York]&timeZones=America/New_York,Europe/London,Asia/Tokyo.
  • Press Enter to send the request.

You'll see a JSON array containing America/New_York, Europe/London, and Asia/Tokyo. The London entry shows 2026-12-15T14:30:00+00:00[Europe/London].

You have the core conversion working. One meeting now produces attendee-local times without hand-built offsets.

Browser showing an error?

Check that meetingTime ends with the bracketed America/New_York zone. Temporal needs that zone to interpret the original clock reading.

Check that timeZones uses commas between the three zone names.

Get help with your request: Why does my meeting-time Worker return an error for this URL?

Your Worker can now translate one meeting across several cities. Next, you'll make that meeting recur one calendar month later.

Add Monthly Recurrence

Your project on Cloudflare Workers now converts one meeting into each attendee's local time. The next useful question is when everyone meets again.

Calendar months have different lengths. Temporal can add a calendar month while preserving the meeting's wall-clock intent.

In this step, get ready to:
  • Read a recurring-meeting option from the request URL.
  • Calculate the next monthly meeting with Temporal.
  • Verify the month-boundary response in your browser.
Update the Worker for recurring meetings

The recurring=true option asks the endpoint to calculate a monthly recurrence. The Worker stores that calculation as a second zoned meeting.

  • In src/index.js, replace the existing Worker source with the code below:
export default {
	async fetch(request) {
		// Read the meeting details from the request URL.
		const url = new URL(request.url);
		const meeting = url.searchParams.get("meeting");
		const timeZones = url.searchParams.get("zones")?.split(",") ?? [];
		const recurring = url.searchParams.get("recurring") === "true";

		// Stop early when required meeting details are missing.
		if (!meeting || timeZones.length === 0) {
			return Response.json(
				{ error: "Add meeting and zones query parameters." },
				{ status: 400 },
			);
		}

		// Build one zoned meeting and an optional monthly recurrence.
		const meetingTime = Temporal.ZonedDateTime.from(meeting);
		const nextMeeting = recurring ? meetingTime.add({ months: 1 }) : null;

		// Show each attendee both local meeting times.
		const localTimes = timeZones.map((timeZone) => ({
			timeZone,
			currentMeeting: meetingTime.withTimeZone(timeZone).toString(),
			nextMeeting: nextMeeting?.withTimeZone(timeZone).toString() ?? null,
		}));

		return Response.json(localTimes);
	},
};

What Does This Code Do?

  • The recurring value becomes true when the request contains recurring=true.
  • The add({ months: 1 }) call creates the next valid calendar date. The original meetingTime value stays unchanged.
  • The withTimeZone() method converts each meeting for every attendee. Each response item contains the current meeting plus the next meeting.
  • Save src/index.js.
  • Switch back to the browser tab containing your local request.
  • Refresh the existing request without the recurring option.

You'll see each attendee object with a currentMeeting value. Its nextMeeting value is null until recurrence is requested.

That is a solid checkpoint: your conversion response now handles one-off meetings without inventing a next date.

Local Response Stopped Loading?

  • Check the closing braces around the map() call.
  • Confirm that your request still contains the meeting parameter.
  • Confirm that your request still contains the zones parameter.

Help me debug the missing local response.

✔️ Awesome, I've got everything!

  • Confirm that src/index.js is saved.

ⓧ I'd like to double check the full code

export default {
	async fetch(request) {
		// Read the meeting details from the request URL.
		const url = new URL(request.url);
		const meeting = url.searchParams.get("meeting");
		const timeZones = url.searchParams.get("zones")?.split(",") ?? [];
		const recurring = url.searchParams.get("recurring") === "true";

		// Stop early when required meeting details are missing.
		if (!meeting || timeZones.length === 0) {
			return Response.json(
				{ error: "Add meeting and zones query parameters." },
				{ status: 400 },
			);
		}

		// Build one zoned meeting and an optional monthly recurrence.
		const meetingTime = Temporal.ZonedDateTime.from(meeting);
		const nextMeeting = recurring ? meetingTime.add({ months: 1 }) : null;

		// Show each attendee both local meeting times.
		const localTimes = timeZones.map((timeZone) => ({
			timeZone,
			currentMeeting: meetingTime.withTimeZone(timeZone).toString(),
			nextMeeting: nextMeeting?.withTimeZone(timeZone).toString() ?? null,
		}));

		return Response.json(localTimes);
	},
};

How the Full Worker Fits Together

This file combines request parsing with calendar-aware recurrence. It converts both meeting values into every requested time zone before returning the response.

Test a month boundary

A month-boundary test exposes calculations that assume every month has the same number of days. This request starts on January 31 when the next month is shorter.

Before you send this request, which calendar date do you expect to appear for the next meeting.

  • Load the month-boundary request by pasting this URL into your browser's address bar:
http://localhost:8787/?meeting=2026-01-31T10:00:00[Europe/London]&zones=Europe/London,America/New_York,Asia/Tokyo&recurring=true

What Does This Request Test?

  • The meeting starts on January 31 in Europe/London.
  • The zones parameter requests three local views of the same meetings.
  • The recurring=true option triggers the calendar-month calculation.

In the Europe/London object, currentMeeting contains 2026-01-31. The nextMeeting value contains 2026-02-28.

That is the calendar trap handled: your monthly meeting now lands on a valid date when the target month is shorter.

Next Meeting Missing?

  • Confirm that the request ends with recurring=true.
  • Check that the meeting value includes the bracketed Europe/London time zone.
  • Refresh the browser after saving src/index.js.

Help me debug the missing next meeting.

Your Worker can now schedule a monthly recurrence across uneven calendar months. Next, you'll compare Temporal with a plain date calculation during a daylight-saving change.

Test a Daylight Saving Change

Your API already keeps monthly meetings aligned with the calendar. The toughest proof comes when a clock change shifts the time-zone offset beneath that calendar.

In this step, you will place Temporal beside JavaScript's Date calculation. A daylight saving boundary will expose the one-hour difference between them.

In this step, get ready to:
  • Add an optional daylight saving comparison to the Worker.
  • Calculate the next monthly meeting with Temporal and Date.
  • Display both results in a local browser response.
Build the daylight saving comparison

Daylight saving time changes a location's offset from UTC. Temporal reads that rule from the named time zone when it performs calendar arithmetic.

  • In src/index.js, place the following helper above export default:
function getDaylightSavingComparison(url, meeting, timeZone) {
	// Keep regular responses unchanged unless the comparison is requested.
	if (url.searchParams.get("compare-daylight-saving") !== "true") {
		return {};
	}

	// Compare calendar-aware arithmetic with a UTC-based Date calculation.
	const temporalResult = meeting.withTimeZone(timeZone).add({ months: 1 });
	const dateResult = new Date(Number(meeting.epochMilliseconds));
	dateResult.setUTCMonth(dateResult.getUTCMonth() + 1);

	return {
		daylightSavingComparison: {
			temporal: temporalResult.toString(),
			plainDate: dateResult.toLocaleString("en-US", { timeZone }),
		},
	};
}

What does this comparison do?

  • The query-parameter check keeps the comparison optional.
  • The Temporal calculation adds one calendar month in the requested time zone.
  • The Date calculation changes the UTC month while preserving the UTC clock time.
  • The returned object places both results under daylightSavingComparison.
  • Inside the existing timeZones.map() callback, find the object assigned to result.
  • Replace that object with the following version:
const result = {
	timeZone,
	currentMeeting: localMeeting.toString(),
	// Include both calculations only when the comparison is requested.
	...getDaylightSavingComparison(url, meeting, timeZone),
};

Why use an object spread?

The spread adds daylightSavingComparison when the query switch is active. Regular requests receive an empty object from the helper, so their response shape stays unchanged.

Use the tabs below to compare your cumulative src/index.js file before running the boundary test.

✔️ Awesome, I've got everything!

Your Worker now supports an optional side-by-side daylight saving comparison.

ⓧ I'd like to double check the full code

function getDaylightSavingComparison(url, meeting, timeZone) {
	// Keep regular responses unchanged unless the comparison is requested.
	if (url.searchParams.get("compare-daylight-saving") !== "true") {
		return {};
	}

	// Compare calendar-aware arithmetic with a UTC-based Date calculation.
	const temporalResult = meeting.withTimeZone(timeZone).add({ months: 1 });
	const dateResult = new Date(Number(meeting.epochMilliseconds));
	dateResult.setUTCMonth(dateResult.getUTCMonth() + 1);

	return {
		daylightSavingComparison: {
			temporal: temporalResult.toString(),
			plainDate: dateResult.toLocaleString("en-US", { timeZone }),
		},
	};
}

export default {
	async fetch(request) {
		// Read the meeting details from the request URL.
		const url = new URL(request.url);
		const meetingTime = url.searchParams.get("meeting-time");
		const timeZones = url.searchParams.get("time-zone")?.split(",") ?? [];
		const recurringMeeting =
			url.searchParams.get("recurring-meeting") === "true";

		if (!meetingTime || timeZones.length === 0) {
			return Response.json(
				{ error: "Add meeting-time and time-zone query parameters." },
				{ status: 400 },
			);
		}

		// Represent the original meeting with its named time-zone rules.
		const meeting = Temporal.ZonedDateTime.from(meetingTime);
		const meetings = timeZones.map((timeZone) => {
			const localMeeting = meeting.withTimeZone(timeZone);
			const result = {
				timeZone,
				currentMeeting: localMeeting.toString(),
				// Include both calculations only when the comparison is requested.
				...getDaylightSavingComparison(url, meeting, timeZone),
			};

			// Add the normal recurring result when the request asks for it.
			if (recurringMeeting) {
				result.nextMonthlyMeeting = localMeeting
					.add({ months: 1 })
					.toString();
			}

			return result;
		});

		return Response.json({ meetings });
	},
};
Run the daylight saving boundary test

New York changes its UTC offset in March. This request starts with a February meeting whose next monthly occurrence lands on the change date.

  • Save src/index.js.
  • Return to the local Worker browser tab from earlier.
  • Select the existing query string after the local host name.
  • Replace the query string by pasting the value below:
?meeting-time=2025-02-09T09:00:00-05:00[America/New_York]&time-zone=America/New_York&recurring-meeting=true&compare-daylight-saving=true

What does this request test?

The meeting begins at 9:00 AM in New York during February. The recurring calculation moves it to the date when New York changes its daylight saving offset.

The final query parameter asks the Worker to include the Temporal and Date calculations in the same response.

Before you load the request, consider whether both calculations will keep the meeting at 9:00 AM after New York changes its offset.

  • Press Enter to send the request.

You will see the Temporal result at 2025-03-09T09:00:00-04:00[America/New_York]. The plain Date result shows 3/9/2025, 10:00:00 AM beside it.

Why is Date one hour later?

Temporal preserves the 9:00 AM wall-clock meeting while applying New York's new offset. The Date calculation preserves the previous UTC clock time, which converts to 10:00 AM after the offset changes.

That one-hour gap is the exact bug that time-zone-aware calendar arithmetic prevents.

Missing the comparison results?

Confirm that the request contains compare-daylight-saving=true. Check that the helper sits above export default.

If the Worker reports a code error, compare the property names in the helper with the spread inside result.

Help me debug my daylight saving comparison.

Your local Worker now proves that its meeting calculations survive a daylight saving change. Next up, you will deploy the same API to a public Cloudflare Workers URL.

Deploy Your Meeting API

Your local meeting API already converts one meeting across cities. It also keeps monthly recurrence accurate through a daylight-saving change.

That proof currently lives only on your computer. Deploying to Cloudflare Workers gives the same JSON response a public workers.dev address.

In this step, get ready to:
  • Deploy the existing Worker configuration to Cloudflare Workers.
  • Record the assigned public workers.dev URL.
  • Confirm that the public endpoint matches the local response.
Deploy the Worker

The wrangler.jsonc file is the release recipe for this Worker. Its enable_temporal flag keeps Temporal available after deployment.

A first deployment may ask you to choose a workers.dev subdomain. That choice becomes part of the Worker's public address.

  • Switch back to the terminal panel running the local Worker.
  • Stop the local server by pressing Ctrl+C.
  • Choose an available subdomain if Wrangler shows the first-deployment prompt.
  • Deploy the current contents of time-zone-meeting-api by running this command:
npx wrangler deploy

What does this command do?

  • The npx command runs the Wrangler installation from your project.
  • Wrangler reads wrangler.jsonc to apply the Worker configuration.
  • Wrangler bundles the Worker code from src/index.js for deployment.
  • The enable_temporal flag gives the deployed Worker the same Temporal support as the local Worker.
  • Wait for Wrangler to finish uploading the Worker.
  • Record the assigned public URL here: https://time-zone-meeting-api.your-subdomain.workers.dev.

Your terminal now shows the public address for the deployed time-zone-meeting-api Worker. The release is live.

Deployment not finishing?

  • Confirm that the terminal prompt is inside the time-zone-meeting-api folder.
  • Confirm that wrangler.jsonc still contains the enable_temporal compatibility flag.

Help me diagnose my Wrangler deployment.

Verify the public endpoint

A successful upload proves that the Worker reached Cloudflare. Reusing the local daylight-saving request proves that every API behavior survived the deployment.

  • Switch back to the browser tab showing the local daylight-saving comparison.
  • Select the local origin before the first ? in the address bar.
  • Replace the selected origin with https://time-zone-meeting-api.your-subdomain.workers.dev.
  • Leave the query string after the first ? unchanged.

Before you load the request, predict whether the public response will match the local response.

  • Load the public request by pressing Enter.

What should you see?

  • The browser displays the same JSON structure as the local Worker.
  • The response contains a local meeting time for every requested time zone.
  • The recurring result moves the meeting forward by one calendar month.
  • The daylight-saving comparison places the Temporal result beside the plain Date result.

You shipped it. Your public meeting API now preserves its time-zone conversion across deployment.

Public response looks different?

  • Compare the public origin with the URL printed by Wrangler.
  • Restore the exact query string from the successful local test.
  • Confirm that the browser address begins with https://.

Help me compare my local response with my deployed response.

Your meeting API is now available from its assigned public workers.dev URL. Anyone with the URL can request the same meeting conversions you tested locally.

Secret mission

Stress-Test Global Meeting Times

Real scheduling APIs meet half-hour offsets. They also meet awkward month boundaries. Stress-test both against your deployed Worker without changing its code.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your resources available, pause your local work, or delete everything entirely. The Cloudflare Workers free plan covers everything in this project, so the resources below have no ongoing project costs.

Resources you used:

  • Deployed Cloudflare Worker named time-zone-meeting-api with an assigned public workers.dev URL.
  • Local time-zone-meeting-api project folder containing your Worker source and configuration.

Keep everything running

No action needed. Choose this if you want to reuse the time zone helper or continue developing it later.

  • Keep the deployed time-zone-meeting-api Worker available at its assigned public workers.dev URL.
  • Keep the local time-zone-meeting-api folder wherever you saved it.

Pause - I'll come back to this later

Stop your local work while preserving the deployed API and its project files. No local process needs to remain open after deployment.

  • Close the browser tab containing the public API response.
  • Close the terminal window you used for the project.
  • Leave the deployed time-zone-meeting-api Worker unchanged.
  • Leave the local time-zone-meeting-api folder unchanged.

Delete - I don't want to use this again

Remove the deployed Worker and the local project folder if you want a complete cleanup. Deleting the Worker takes its public URL offline, so save any results you want to keep first.

  • In the Cloudflare dashboard, locate the Worker named time-zone-meeting-api.
  • Use the deletion control on the Worker's management page.
  • Confirm the deletion when Cloudflare asks.

You should no longer see time-zone-meeting-api in your Worker list. Its assigned public workers.dev URL should stop returning your meeting API response.

  • Search for time-zone-meeting-api in your computer's file manager.
  • Delete the time-zone-meeting-api folder from its saved location.
  • Empty your computer's trash.

A second search should no longer find the local time-zone-meeting-api folder.

Nice Work!

Nice Work!

You made it! Your time zone meeting API is live on Cloudflare Workers. It now handles the calendar problems that make global scheduling difficult.

You've learned how to:

  • Deployed a public Worker API that returns meeting results from its workers.dev URL.
  • Used Temporal to convert one meeting into the correct local time for attendees across multiple time zones.
  • Built calendar-aware recurrence that moves a meeting forward by one month. You also compared daylight saving results with a plain Date calculation.
  • Secret Mission: Extended your time zone helper through an optional challenge that pushes your Temporal skills further.

Ready to quiz yourself?