Email Notifications

Email is optional. The component includes Resend-backed notifications for booking confirmation, pending approval, approval, decline, cancellation and rescheduling. Bookings also work without an email provider.

Enable Email

Create a Resend API key and verify your sending domain. Set the following on your Convex deployment, using your own values:

bash
npx convex env set RESEND_API_KEY re_your_key
npx convex env set RESEND_FROM_EMAIL bookings@yourdomain.com
npx convex env set NEXT_PUBLIC_APP_URL https://yourdomain.com

Set production values separately when deploying. Your Next.js .env.local does not set the environment of remote Convex functions. NEXT_PUBLIC_APP_URL is the name used by the reference gateway for email links; the API key must stay on the server.

Resend is already nested inside the booking component. You do not need to mount another Resend component for these notifications.

Pass Configuration from the Host

The reference gateway already reads these variables and passes resendOptions to the component. For your own host functions, use the same pattern:

typescriptconvex/emailOptions.ts
export function bookingEmailOptions() {
  const apiKey = process.env.RESEND_API_KEY;
  if (!apiKey) return undefined;
  return {
    apiKey,
    fromEmail: process.env.RESEND_FROM_EMAIL,
    baseUrl: process.env.NEXT_PUBLIC_APP_URL,
  };
}

Pass resendOptions: bookingEmailOptions() when calling booking creation, cancellation, rescheduling or state-transition mutations that accept it. Do not accept the API key or sender configuration from browser arguments.

Without resendOptions.apiKey, the built-in email handler skips delivery. An email send failure does not undo the booking; inspect delivery separately.

Set baseUrl to your app's public origin, without a trailing slash. Built-in templates use these routes, with the booking's secret token in the query string:

/book/booking/[uid]?token=...
/book/booking/[uid]/cancel?token=...
/book/booking/[uid]/reschedule?token=...

Your app must implement those routes. The demo booking pages show a token-based implementation. If your app uses a different route structure, redirect these routes to your own management pages or adapt the links in an app renderer, described below.

Use Your Own Email Design

Starting with version 0.4.2, an optional app renderer can replace the email's subject, HTML and plain text. Booking still selects the notification and sends it through its nested Resend component. Existing integrations need no changes.

Create an internal query in your app. Returning null keeps the default template for that notification, so you can customize one kind at a time:

typescriptconvex/bookingEmailRenderer.ts
import { internalQuery } from "./_generated/server";
import {
  bookingEmailContextValidator,
  bookingEmailResultValidator,
} from "@mrfinch/booking/emails";
 
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (char) => ({
  "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[char]!));
 
export const render = internalQuery({
  args: bookingEmailContextValidator,
  returns: bookingEmailResultValidator,
  handler: (_ctx, email) => {
    if (email.kind !== "confirmed") return null;
    return {
      subject: "Your booking is confirmed",
      html: `<h1>Thanks, ${escapeHtml(email.bookerName)}</h1>
        <p>${escapeHtml(email.eventTitle)}</p>
        ${email.links ? `<a href="${escapeHtml(email.links.view)}">View booking</a>` : ""}`,
      text: `Thanks, ${email.bookerName}. Your booking is confirmed.` +
        (email.links ? `\nView booking: ${email.links.view}` : ""),
    };
  },
});

Then make your host's email-options helper asynchronous and pass the renderer:

typescriptconvex/emailOptions.ts
import { internal } from "./_generated/api";
import { createBookingEmailOptions, type BookingEmailOptions } from "@mrfinch/booking/emails";
 
export async function bookingEmailOptions(): Promise<BookingEmailOptions | undefined> {
  const apiKey = process.env.RESEND_API_KEY;
  if (!apiKey) return undefined;
  return createBookingEmailOptions({
    apiKey,
    fromEmail: process.env.RESEND_FROM_EMAIL,
    baseUrl: process.env.NEXT_PUBLIC_APP_URL,
    renderer: internal.bookingEmailRenderer.render,
  });
}

Use resendOptions: await bookingEmailOptions() in each host booking operation. The renderer receives a snapshot of the notification's data, including old/new times on rescheduling. No template registration or extra Resend instance is needed.

The supported kinds are confirmed, pending, approved, declined, cancelled and rescheduled. A missing renderer or a null result uses the built-in template. A broken renderer fails the email job visibly; it does not undo the booking.

Renderers run in the Convex query runtime, without network requests or Node-only libraries. Custom content is limited to 128 KiB of combined UTF-8 subject, HTML and text, with a nonempty subject of at most 200 UTF-16 code units. For payload details, escaping and failed-job recovery, see the custom email guide.

Notifications and Hooks

The built-in notification follows the booking lifecycle:

ChangeNotification
Create a confirmed bookingConfirmation
Create a pending bookingAwaiting approval
Confirm a pending bookingApproval
Confirm a provisional bookingConfirmation
Decline, cancel or rescheduleMatching update

Delivery is scheduled after the booking operation. A successful booking response means the booking exists; it is not confirmation that the email reached an inbox.

For additional side effects, such as an organizer notification, register a host mutation function handle with components.booking.hooks.registerHook. Hooks receive lifecycle payloads such as booking.created, booking.confirmed or booking.cancelled; these are Convex functions, not HTTP webhook URLs. Keep registration internal or administrator-only. An organization-scoped hook receives only that organization's events; a hook without organizationId is global.

The hook implementation and email templates are the source for payloads and template behavior. If your hook takes over guest delivery entirely, omit built-in resendOptions to avoid sending twice. For a different guest email design alone, use the renderer above.

Verify Delivery

Create a booking with an address you control, open its management link, then test rescheduling and cancellation. Check the times, sender and URL as well as the message itself. For approval flows, also test pending, approval and decline.

If a message does not arrive, check the Convex function logs and Resend delivery status. Confirm the key is set on the correct deployment and the sender domain is verified. A key alone is insufficient: the fallback sender is bookings@example.com, so configure RESEND_FROM_EMAIL explicitly.