---
title: "Get email into inboxes"
description: "Send from your own verified domain, leave test mode, and know what happens to a sign-in code or an invitation the deployment cannot deliver."
canonical_url: "https://nuxt-backend.dev/docs/email/deliverability"
---
# Get email into inboxes

> Send from your own verified domain, leave test mode, and know what happens to a sign-in code or an invitation the deployment cannot deliver.

A new deployment sends nothing to real people. Until you set it up, email goes only to the provider's test addresses (`delivered@resend.dev` and its `+label` aliases). A one-time code (to sign in, or for an email change) or an invitation for anyone else is refused before it is created; any other email to a real address, a welcome or your own `email.send`, fails when it is sent. Three settings decide it, all on the deployment:

<table>
<thead>
  <tr>
    <th>
      Variable
    </th>
    
    <th>
      Without it
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        EMAIL_API_KEY
      </code>
    </td>
    
    <td>
      Nothing is sent. A test address still gets its copy in the <a href="https://nuxt-backend.dev/docs/email/sandbox-inbox">
        sandbox inbox
      </a>
      
      , and a dev deployment can echo sign-in codes to its logs.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        EMAIL_TEST_MODE=false
      </code>
    </td>
    
    <td>
      Test mode, the default: the provider accepts only its test addresses.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        EMAIL_FROM
      </code>
    </td>
    
    <td>
      Mail comes from the provider's onboarding address. The provider delivers it only to test addresses and to its account owner's own address, so the package refuses codes and invitations to every real address.
    </td>
  </tr>
</tbody>
</table>

## Send from your own domain

1. Add your domain in the Resend dashboard and publish the DNS records it lists: SPF and DKIM for the domain, and a DMARC policy (`p=none` to start).
2. Wait for the dashboard to show the domain as verified.
3. Set the sender and turn test mode off, on the deployment:

```bash
npx convex env set EMAIL_FROM "Acme <hello@acme.dev>"
npx convex env set EMAIL_TEST_MODE false
```

Add `--prod` to set them on production. `EMAIL_FROM` takes the display-name form, and the address must be on the verified domain.

## Codes and invitations that would not arrive

Better Auth sends sign-in codes and workspace invitations in the background and answers success whatever the send does. Left alone, a code the provider refuses would look sent, and the person would wait for an email that never comes. So the package checks the request first, and refuses it when the email could not arrive:

<table>
<thead>
  <tr>
    <th>
      The deployment
    </th>
    
    <th>
      A sign-in code to a real address
    </th>
    
    <th>
      An invitation to a real address
    </th>
    
    <th>
      Either, to a test address
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      No <code>
        EMAIL_API_KEY
      </code>
    </td>
    
    <td>
      Refused, unless <code>
        NUXT_BACKEND_LOG_OTP=1
      </code>
      
       echoes it to the logs
    </td>
    
    <td>
      Refused
    </td>
    
    <td>
      Kept in the sandbox inbox while test mode is on, otherwise dropped
    </td>
  </tr>
  
  <tr>
    <td>
      Key, test mode on
    </td>
    
    <td>
      Refused
    </td>
    
    <td>
      Refused
    </td>
    
    <td>
      Sent
    </td>
  </tr>
  
  <tr>
    <td>
      Key, test mode off, no <code>
        EMAIL_FROM
      </code>
    </td>
    
    <td>
      Refused
    </td>
    
    <td>
      Refused
    </td>
    
    <td>
      Sent
    </td>
  </tr>
  
  <tr>
    <td>
      Key, test mode off, <code>
        EMAIL_FROM
      </code>
      
       set
    </td>
    
    <td>
      Sent
    </td>
    
    <td>
      Sent
    </td>
    
    <td>
      Sent
    </td>
  </tr>
</tbody>
</table>

A refusal is a `400` with a message the sign-in form and the invitation form show as they are:

- **Test mode:** "This app is in test mode and only emails test addresses, so this address can't receive mail here."
- **Not set up:** "This app can't send email yet."

No code is stored and no invitation is created. The deployment's logs say which setting to change, without the address: look for [`email.undeliverable`](https://nuxt-backend.dev/docs/ship/operate/troubleshooting#log-codes).

A sign-in code is checked after your [`canSignIn`](https://nuxt-backend.dev/docs/auth/setup#cross-component-wiring) gate, so your own refusal is the one a person reads. The code an email change sends to the new address is checked the same way, before it exists. An invitation is checked only once the inviter is signed in, so an anonymous request learns nothing of the deployment. Only email the package sends is checked: an app that passes its own `emailOTP` plugin, its own `organization` plugin or `organization.sendInvitationEmail` delivers those itself.

## Related

- [Sending](https://nuxt-backend.dev/docs/email/sending) lists every `EMAIL_*` variable.
- [Test with the sandbox inbox](https://nuxt-backend.dev/docs/email/sandbox-inbox) reads test-mode mail without a mailbox.
- [Delivery tracking](https://nuxt-backend.dev/docs/email/delivery-tracking) follows each email to delivered, bounced or complained.

<page-keys>

- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/client/delivery.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/client/delivery.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
