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:
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.comSet 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:
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.
Management Links
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:
import { internalQuery } from "./_generated/server";
import {
bookingEmailContextValidator,
bookingEmailResultValidator,
} from "@mrfinch/booking/emails";
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (char) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
}[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:
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:
| Change | Notification |
|---|---|
| Create a confirmed booking | Confirmation |
| Create a pending booking | Awaiting approval |
| Confirm a pending booking | Approval |
| Confirm a provisional booking | Confirmation |
| Decline, cancel or reschedule | Matching 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.