New from emailrHealthCheck Email iconHealthCheck Email
emailr_

Send email from Remix with emailr_

Send transactional email from Remix actions with the emailr_ SDK, keep the client in a .server module and verify delivery webhooks in resource routes.

Remix iconRemixFramework

Remix actions run on the server and receive form submissions directly, which makes them a clean place to send email. This guide keeps the client in a .server module, sends from an action and receives signed webhooks in a resource route.

The same code works in React Router v7 framework mode, which uses the same action and resource route conventions.

Set up emailr_ with Remix

  1. 01

    Create an API key

    Create a key under API keys in the dashboard. Keys start with et_live_ and are scoped to one domain workspace. Store it as EMAILR_API_KEY in your secret manager or environment, never in client-side code.

  2. 02

    Add and verify your sending domain

    In the dashboard, open Domains and add the domain you send from. A subdomain such as mail.yourcompany.com keeps transactional reputation separate from your corporate mailbox.

    Emailr generates the DNS records: a DKIM TXT record on the emailr._domainkey selector, an SPF TXT record, and an MX + TXT pair on the send subdomain used as the MAIL FROM / Return-Path. A DMARC record is optional but recommended.

    Emailr's DKIM selector and send subdomain don't collide with other providers' records, so you can add them before cutting over and send from both providers during the migration. A domain can only have one SPF record: if one already exists, merge Emailr's include: into it instead of adding a second record.

  3. 03

    Install the SDK

    Install the package and set EMAILR_API_KEY in your environment.

    Terminalbash
    npm install emailr
  4. 04

    Create a server-only client

    The .server.ts suffix tells the compiler never to include this module in browser bundles, so the key can't leak.

    app/lib/emailr.server.tsts
    import { Emailr } from 'emailr'
    
    export const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! })
  5. 05

    Send from an action

    Read the submitted form, send, and return data to the page. Errors thrown by the SDK are EmailrError instances with a statusCode, so you can show a friendly message for validation failures.

    app/routes/contact.tsxtsx
    import type { ActionFunctionArgs } from '@remix-run/node'
    import { json } from '@remix-run/node'
    import { emailr } from '~/lib/emailr.server'
    
    export async function action({ request }: ActionFunctionArgs) {
      const form = await request.formData()
    
      await emailr.emails.send({
        from: 'Website <noreply@mail.acme.com>',
        to: 'support@acme.com',
        reply_to_email: String(form.get('email')),
        subject: 'New contact form message',
        text: String(form.get('message')),
      })
    
      return json({ sent: true })
    }
  6. 06

    Receive webhooks in a resource route

    A route module with only an action export is a resource route. Read request.text() before parsing so the signature check sees the raw body.

    app/routes/webhooks.emailr.tsts
    import type { ActionFunctionArgs } from '@remix-run/node'
    import { createHmac, timingSafeEqual } from 'node:crypto'
    
    export async function action({ request }: ActionFunctionArgs) {
      const body = await request.text()
      const signature = request.headers.get('x-emailr-signature') ?? ''
      const expected = createHmac('sha256', process.env.EMAILR_WEBHOOK_SECRET!)
        .update(body)
        .digest('hex')
    
      if (signature.length !== expected.length ||
          !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
        return new Response('Invalid signature', { status: 401 })
      }
    
      const { event, data } = JSON.parse(body)
      return new Response('ok')
    }

Tips

  • On Cloudflare-hosted Remix apps, read the key from the context.cloudflare.env binding instead of process.env, and verify webhooks with Web Crypto.
  • For password resets and magic links, send from the action that creates the token so a failed send surfaces immediately.

Frequently asked questions

Where should the emailr_ client live in a Remix app?

In a .server.ts module. Remix excludes .server modules from browser bundles, so your API key never reaches the client.

Does this work with React Router v7?

Yes. Actions and resource routes work the same way in React Router v7 framework mode; only the import paths for types change.

How do I handle bounces in Remix?

Point a transactional webhook at a resource route, verify the X-Emailr-Signature header against the raw body, and handle email.bounced events.

Can I send HTML built with React?

Yes. Render your email component to an HTML string and pass it as the html field.

Start sending in minutes

Create a free account, verify your domain and send your first email. No credit card required.