Email is handled by @repo/mail (packages/mail). It renders React Email templates with translations from @repo/i18n and sends through one provider.

Sending

The package exports a single function:
Two call shapes are accepted: context is typed from the template’s props, without locale and translations, which the function supplies.
sendEmail never throws on a provider failure. It catches the error, logs Failed to send email with logError, and returns false. A caller that needs to know whether the email went out must check the return value. Most callers do not, which is the right choice for a side effect and the wrong one for anything the user is waiting on.

Templates

packages/mail/src/emails/index.ts registers the templates: Shared building blocks are in src/components/: Wrapper, Logo, PrimaryButton.

Rendering

getTemplate({ templateId, context, locale }) in lib/templates.ts:
  1. Loads the mail translation scope for the locale with getMessagesForLocale(locale, 'mail').
  2. Calls the component with the context plus locale and translations.
  3. Takes the subject from translations[templateId].subject. For the notification template a non empty context.title replaces it.
  4. Renders twice with @react-email/render: once to HTML and once to plain text.

Locale and direction

The default locale is he, taken from @repo/i18n. Supported locales are every key of config.locales: he, en, ar, de, es, fr. Templates are direction aware. ctaArrow(locale) in lib/direction.ts returns a left arrow for RTL locales and a right arrow for LTR ones, based on getLocaleDirection. Auth emails take the locale from the NEXT_LOCALE cookie on the request. Notification emails use the user’s stored locale.

Provider

packages/mail/src/provider/index.ts exports one provider:
The ZeptoMail provider posts to ZEPTOMAIL_API_URL (or the ZeptoMail default endpoint) with ZEPTOMAIL_TOKEN. The sender is params.from when given, otherwise MAIL_FROM. The folder also contains console.ts, mailgun.ts, nodemailer.ts, plunk.ts, postmark.ts and resend.ts. They implement the same SendEmailHandler and are not exported. To switch provider, change the one export line and set that provider’s env variables. For local work, exporting ./console prints emails instead of sending them.

Env

These are read with process.env inside the package and are not part of the core-api Zod schema. A missing token does not stop the server from booting. It makes every send fail and return false.

Team invitation email

apps/core-api/src/lib/team-invite-mail.ts is the one place domain code sends email. When a coach adds a team member, sendTeamInvitationEmail(prisma, options) sends the teamInvitation template with a sign-in link built as <APP_WEB_URL>/login?email=....
  • The locale comes from the studio row. A value outside the six mail locales falls back to the mail package default.
  • It is best effort. The team member is created whether or not the email goes out. Missing mailer credentials, a provider outage or a studio row that disappeared are logged and swallowed.
  • createTeamInviteMailer({ prisma, appUrl, logger }) wraps it for injection. The AutoFit import worker receives it as mailer.
The link goes to the normal login page. The staff member signs in with an email or phone OTP, and the auth tier creates their account from the directory entry on first use. See Authentication.

Adding a template

1

Write the component

Add src/emails/MyTemplate.tsx. Accept BaseMailProps (locale, translations) plus your own props. Use Wrapper for the layout.
2

Register it

Add it to mailTemplates in src/emails/index.ts. The key is the templateId.
3

Add translations

Add a block under the same key in packages/i18n/src/translations/<locale>/mail.json for every locale, including subject. A missing key in a non default locale falls back to the Hebrew text.
4

Rebuild

pnpm --filter @repo/i18n build copies the translation files into dist, and pnpm --filter @repo/mail build rebuilds the package. core-api consumes both from dist.