Skip to main content

Notifications & Email

In one line: Royal Glow reaches people through email (the durable record — Resend for transactional, Brevo for marketing) and Web Push (the instant nudge). All three are guarded extension points: with a key absent the send is a no-op that logs and returns cleanly.
Royal Glow keeps people informed through two channels: email (the durable record — confirmations, invoices, reminders, offers) and Web Push (the instant nudge that lands on a phone or laptop even when the site is closed). This page covers how each is wired up and which service handles what.
Email and push are guarded extension points. Each helper reads its keys directly behind a truthy guard — Resend behind RESEND_API_KEY, Brevo behind BREVO_API_KEY, and Web Push behind the VAPID keys. With a key absent, the send is a no-op that logs and returns cleanly, so the app builds and runs with no email or push configured.

Why two email services

Because authentication is Google OAuth only, the app sends zero auth emails — no password resets, no magic links. Every email is a business event. Those split naturally into two very different infrastructure problems, so they use two services.

Email providers

Resend handles transactional mail; Brevo handles marketing. The split is strict — invoices and confirmations never go through the marketing pipeline, and promotional blasts never go through the transactional one.
Resend is built for instant, reliable 1-to-1 delivery, has first-class support for PDF attachments (invoices), and integrates natively with React Email. Its free tier — 3,000 emails/month, 100/day — covers the entire launch phase. All transactional mail is sent from hello@theroyalglow.in.
Marketing email requires explicit consent. A customer is only added to Brevo lists when they tick the marketing consent box at onboarding (marketing_consent = true). Transactional email — invoices and booking confirmations — is always sent and cannot be fully disabled.

React Email templates

Transactional templates are built with React Email as typed .tsx components in packages/emails/, styled in Royal Glow’s brand. They receive strongly typed props (money as integer paise, dates as DD/MM/YYYY) and render to HTML at send time. A typical send generates the invoice PDF, then hands the React component and the attachment to Resend:
Brevo marketing emails do not use React Email. They are designed in Brevo’s drag-and-drop editor and managed entirely in the Brevo dashboard, so marketing content can change without a deploy.

Web Push notifications

Web Push delivers a short, tappable alert to a customer’s device — booking confirmed, reminder due, gems earned — even when the tab is closed. It uses the browser’s native Push API and the VAPID protocol; no third-party push vendor is involved.

Subscription flow

1

Permission

The browser’s native prompt is shown after the customer’s first successful booking, not on first load.
2

Subscribe

Once granted, the service worker calls pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC_KEY }), returning a PushSubscription.
3

Store

The client posts it to POST /api/push/subscribe, which upserts a row in the push_subscription table keyed by endpoint and bound to the signed-in user.
4

Send

When an event fires, the server fetches the user’s subscriptions and the web-push library signs and delivers the payload; the service worker shows the notification.

Storage

POST /api/push/subscribe persists the subscription serialised by the browser’s PushManager: DELETE /api/push/subscribe soft-unsubscribes by deactivating the row for a given endpoint. Both routes are session-scoped, so one user can never touch another’s subscriptions. See the Notifications & Realtime API for the full request and response contracts.
Web Push is gated on the VAPID keys (VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY). When they are absent, subscription and delivery no-op and log — the booking still succeeds, it simply sends no push. Reminder jobs always check the customer’s preferences before sending, and skip a booking that is no longer confirmed.
Last modified on June 29, 2026