Skip to content

Module - Mail

To it’s core, firstly provides you the ability to send emails. For this, we didn’t reinvent the wheel and use the great nodemailer package.

Once you have it setup, assign you the role "Mail.Admin" (You can get it via Roles_Mail.Mail_Admin), and in Admin UI, you will be able to see all mails in the entity named FF Mails.

Terminal window
npm add firstly@latest -D
src/server/api.ts
import { mail } from 'firstly/mail/server'
export const api = remultApi({
modules: [mail()],
})

Anywhere in your code you can then:

import { remult } from 'remult'
await remult.context.sendMail('my_first_mail', {
to: 'hello@example.com',
subject: 'Hello from firstly',
sections: [
{ html: 'hello <b>world</b> 👋' },
{
html: 'Did you star remult repo ?',
cta: { html: 'Star it', link: 'https://github.com/remult/remult' },
},
],
})

The result will be something like this:

Mail preview

You can see the structure of the mail in the following image:

Mail structure

Configure the transport of your email service.

export const api = remultApi({
modules: [
mail({
nodemailer: {
transport: {
host: '...',
port: 587,
secure: false, // Use `true` for port 465, `false` for all other ports
auth: {
user: '...',
pass: '...',
},
},
},
}),
],
})

Resend speaks SMTP, so it drops in as a regular nodemailer transport. Sign up, verify your sending domain (Resend gives you the SPF / DKIM / DMARC DNS records to paste into your registrar), grab an API key, then:

// Load RESEND_API_KEY from your framework's env helper:
// SvelteKit: import { RESEND_API_KEY } from '$env/static/private'
// Next / Node: const RESEND_API_KEY = process.env.RESEND_API_KEY!
export const api = remultApi({
modules: [
mail({
from: { name: 'My Cool App', address: 'noreply@mycoolapp.com' },
nodemailer: {
transport: {
host: 'smtp.resend.com',
port: 465,
secure: true,
auth: {
user: 'resend', // literal string, not your account email
pass: RESEND_API_KEY,
},
},
},
}),
],
})

Two drop-in Svelte 5 components for a quick “send + browse” admin page. Raw Tailwind only, no plugin required.

<script lang="ts">
import { LastMails, WriteMail } from 'firstly/mail'
</script>
<WriteMail />
<LastMails limit={30} />

Both gate on the Mail.Admin role (assigned via the auth boutique’s addRolesToUser helper or SUPER_ADMIN_EMAILS). Users without the role see an inline notice instead of the form / list, so it’s safe to land on a route any authenticated user can reach.

<LastMails /> calls repo(Mail).find({ limit }) on mount and on a Refresh button click - sorted createdAt desc (entity default). It exposes refresh() so you can bind:this and call it from a sibling after a manual send if you want.

Global params are applied to all mails by default and can be overridden for each mail (handy!)

export const api = remultApi({
modules: [
mail({
service: 'Cool App',
footer: `Thank you for using Cool App`,
// primaryColor: '#000000',
// secondaryColor: '#000000',
// toHtml(mailInfo) => `` // You can override the html of the mail
from: {
name: 'My Cool App',
address: 'noreply@coolApp.com',
},
}),
],
})