• Модуль интеграций
  • Сообщество
  • Блог
Документация
Плагины и интеграцииВсе расширения для Medusa от сообществаСтартерыЗапускайте проекты быстрее с готовыми решениями
ЭкспертыПодберите специалиста для разработки и развития вашего проекта на MedusaКейсыПосмотрите примеры Medusa в продакшене и успешные внедрения
Т-Касса от Т-Банка для Medusa: приём платежей для РФ

16 октября 2026 г. · Продукт

Gorgo снижает затраты на адаптацию Medusa к локальным рынкам.

Мы разрабатываем плагины интеграции, осуществляем поддержку и развиваем сообщество разработчиков на Medusa в Telegram.

  • Ресурсы Medusa
  • Плагины и интеграции
  • Модуль интеграций
  • Стартеры
  • Эксперты
  • Кейсы
  • Medusa Чат в Telegram
  • Medusa Новости в Telegram
  • Документация Gorgo
  • Связаться с нами
  • TelegramGitHub
Плагины
E

Emails

Транзакционные письма для Medusa от Koda Plus, отправляемые через Resend. Есть готовые шаблоны на польском и английском языках (подтверждение заказа, отправка заказа с отслеживанием, отмена, приветствие, сброс пароля, брошенная корзина), тёмная тема, живой предпросмотр и тестовая отправка.

npm install @koda-plus/medusa-plugin-emails
Категория
Уведомления
Создано
Koda-plus
Версия
0.2.0
Последнее обновление
1 неделю назад
Ежемесячные загрузки
Загрузка данных
Звезды на Github
0
npmNPMGitHubGithub

E-mails by Koda Plus

Transactional e-mails for Medusa, sent through Resend, ready the moment you install them: the order confirmation, the shipping e-mail with tracking numbers, the cancellation, the welcome and the password reset, in English and Polish, in your store's look and with a dark mode. Every message is sent once per event, logged with the address masked, and visible in the Medusa admin, where you preview every template live and send yourself a test.

The templates carry no images: the store name is live text and products are tiles with their name, SKU, quantity and price, so nothing breaks when a mail app blocks images. Your own templates (a B2B approval, an invoice, a campaign) plug into the same registry and the same look.

Live demo: Medusa admin medusa.koda.plus/app/emails, signed in to a public demo account by the link itself. The demo runs the plugin in demo mode: every e-mail the store would send lands in a simulated outbox you can open, and nothing leaves the server.

What it does

  • Sends the store's e-mails through Resend, as a Medusa notification provider: subscribers to Medusa's events render the right template in the customer's language and hand it to the notification module, the provider sends it.
  • Exactly once per event: one idempotency key per event (), checked by Medusa's notification module, claimed atomically in the plugin's send log, and sent to Resend as the header.
  • Logs what went out with the recipient masked (): the template, the subject, the status, the Resend id, the error when there was one.
  • Shows it in the admin: counters, the log, counts per template, the template gallery with a live preview (light and dark, desktop and phone, Polish and English, sample data or your newest order), a test send, and the e-mails of each order on the order page.

Templates

Wired to Medusa's events, on by default:

  • Order confirmation (, on ): the order's road on a tracker, number, date, delivery, the products with their amounts, the totals with tax and discount notes, the delivery address, a link to the order.
  • Order shipped (, on ): every tracking number with its link (from the label, or from your ), the products of this parcel, and a note when the rest follows in another parcel.
  • Order cancelled (, on ): the order as a slip with its status, the cancelled products, where the money goes.
  • Welcome (, on , registered accounts only): a customer card and three first steps.
  • Password reset (, on ): a button to your storefront's reset page for customers, to the admin's for admin users, how long the link works, what to do if it was not you.

Optional, off until you turn them on:

  • Abandoned cart (, an hourly job): carts idle between 24 and 72 hours with an address and products, one reminder per cart, at most 50 an hour.
  • Negotiation e-mails (, , ): for the events of when it is installed. The offer with its price per unit (or for the whole cart), the quantity and how long a counter offer is valid. A customer who declines an offer gets no e-mail about their own click. The plugins know each other by event name only; this one has no dependency on that one.

Features

  • E-mails page in the admin with three views switched in the header: Panel (what was sent, the template gallery, the test send), Setup guide (also as ) and Settings (branding, templates, the provider).
  • Live preview of every template, built-in or yours, exactly as the provider renders it, with your branding: light or dark, desktop or phone, Polish or English, on sample data or on your newest order, cart or customer (with the customer's name, company and address replaced by sample ones).
  • Test send to an address you type, through the same path as every e-mail, so it also proves the setup. The subject starts with ; at most 5 tests per person in 10 minutes and 30 an hour for the store.
  • Branding from the options or from the admin: the store name, a live-text logo (with an accent part such as a "+" and a lighter suffix), the accent and band colours, a footer line per language, a support address. Colours derive the rest: text in the accent colour is darkened or lightened until it reads at 4.5:1, the neutrals take the hue of the band, a band too light for white text is darkened.
  • Every template switchable: the option is the hard switch (), the admin switch the runtime one, with who flipped it and when.
  • Per-recipient language: the order's or cart's , then or in the order's, cart's or customer's metadata, then .
  • Dark mode for Apple Mail and most apps, Outlook.com included; Polish typography with no one-letter word at the end of a line.
  • Under Gmail's clipping size and a plain-text part in every message; long orders show 30 lines and a sum-up line.
  • Retry from the admin for failed messages of the order, shipping, cancellation, welcome and cart templates: the data is read again, the provider takes the failed row over, Resend gets a fresh key.
  • Order widget on every order page: the e-mails of that order with their status, one click to the message; a host can show it as a tab (see Works with Koda Plus hosts).
  • Deep links into the log: , , , , .
  • Demo mode (only with ): a simulated outbox, seeded from your newest orders and customers and dated over the last days, rebuilt with fresh dates twice a day so a public demo never looks abandoned; test sends and new events land there and stay; nothing leaves the server, and password reset links are kept hidden.
  • Your own templates in the same look, with the same kit, through the option or .
  • Admin in English and Polish through the Medusa admin translations.

Requirements

  • Medusa 2.12 to 2.21 and Node.js 20+ (see Compatibility).
  • A Resend account with a verified sending domain, and an API key with sending access. Without a key the plugin logs every message instead of sending it (the admin says "Log only"); in demo mode it needs nothing. The free plan of Resend sends 100 e-mails a day and 3 000 a month: choose a plan for your volume before you go live.

Installation

Yarn and pnpm work the same way: or .

Configuration

The plugin is registered twice with the same options object: as a plugin (the send log, the admin page, the subscribers and jobs) and as the provider of the email channel in Medusa's notification module (rendering and sending). Keep the object in one constant:

Listing replaces Medusa's default list, which held the local provider of the channel; that is why the example adds it back. Without it, product import and export and the order export end with "Could not find a notification provider for channel: feed". The admin warns when no provider serves .

Set the variables in :

Run the migrations, then open E-mails in the admin sidebar:

npx medusa db:migrate

Medusa uses one provider per channel: if another provider serves (SendGrid, or the local one set to ), remove it, and keep the local one on . The admin says when the provider is missing, got other options than the plugin, or runs in another mode. The of the registration ( above) is the name Medusa's provider table and this admin show; is the identifier of the provider class, which you never type.

Options

Sending:

  • : the Resend API key. Without it nothing is sent: every message is rendered, logged and recorded as not sent.
  • : or the bare address, on a domain verified in Resend. Required in live mode.
  • : one address or a list. With it, the "Questions?" box tells customers to reply.
  • (default ): the simulated outbox. Nothing leaves the server, every message is kept to look at. Only turns it on: a missing API key never does, it means "Log only" (nothing is sent, the admin says what is missing). Set it from a variable of your own, .
  • (default ), (default ), (default ): requests to Resend from one process, the timeout of one request, the extra tries after a temporary error.

Language and links:

  • (default ): or , when the recipient's language is unknown.
  • (default ): for dates in messages.
  • : the public address of the storefront. Links default to it: the store, , , .
  • : , each an absolute address or a path joined to , with the placeholders , , , , , , , , (URI-encoded; an empty one leaves no double slash). For example .
  • (default: and of medusa-config.ts): the admin, for the password reset of admin users.
  • (default ): how long the reset link works, for the text of the message (Medusa's own default).
  • (default , from 1 to 50): password reset e-mails to one address in any hour. Asking for a reset needs no account, so this keeps a stranger from flooding an inbox from your domain and using up your Resend plan; further requests are logged as skipped ().
  • : tracking links per fulfillment provider id (or its first part) when a label has none, e.g. .

Brand (), each field also editable in the admin:

  • : in subjects and texts.
  • : , the logo as live text (default: the name).
  • (default ), (default ): buttons and highlights, and the band at the top.
  • : one line, a string or .
  • : shown in the "Questions?" box.
  • , : CSS font-family lists (default: the system sans). : , web fonts on https, loaded where the mail app allows them.

Templates:

  • : turns a template off for good (the admin cannot turn it on), turns an optional one on, a definition adds a template or replaces a built-in one under its key.
  • : . The links of lead to the cart: with in it, the link opens that cart (and the address typed in it) for anyone who holds the e-mail, so prefer a page that asks the shopper to sign in.
  • (default ): orders with one of these metadata keys get no e-mail. Orders with Medusa's never do. A shopper cannot set these keys through the Store API (see Security).
  • (default ): only for negotiation events that send as a number without , in major units (469 for 469.00) or minor (46900). The Koda Plus negotiations plugin sends as a decimal string and in minor units, so it needs nothing here.

Presentation and data:

  • (default , keeps everything): how long the send log keeps its rows.
  • (default none): stores running the plugin, shown as "Running in production": , texts plain or . marks a store that starts on Medusa soon: it is shown with a "Soon" badge and no link, and its is optional. Entries without a name, or live entries without an https URL, are dropped.

Missing or broken options never break the boot: broken values fall back to their defaults and the admin lists them.

Custom templates

A template is a definition with a function. Build it with the kit to get the look, the dark mode, the escaping and the plain-text part for free, then pass it in (in the shared options object, so the plugin and the provider both get it):

Then send it from your own code, through the notification module or the bundled workflow:

  • The kit: inline text (, , , , , , ) and blocks: , , , , , , , , , , , , , and on the band , , , . Strings are escaped; only passes markup through.
  • may also return finished ; the text part is then made from the HTML.
  • Keep pure: the same data, the same message. A retry must send the same payload.
  • marks data fields that carry a secret (a link with a token, as of the built-in password reset). Such a message never goes through Medusa's notification table (the plugin hands it to its provider directly, with the same send log and idempotency), and the simulated outbox keeps it with those fields hidden. A built-in template you replace under its key keeps its secret fields.
  • registers at run time instead (from a file Medusa loads in every process). Lookup order: the option, then , then the built-in set.
  • renders outside Medusa, for your own tests.
  • A notification whose is ignores the template switches: that is how the admin's test send checks a template that is off. Keep it for tests.
  • App code may call directly; pass an to make it exactly once. Medusa keeps in its notification table and returns it from , so send a template with secret fields through instead.

Setup in brief

The admin has the full guide (E-mails, Setup guide), with the state of every step taken from your store.

  1. Create a Resend account. Until a domain is verified, Resend sends only from its own test address and only to the address of your account. The free plan sends 100 e-mails a day and 3 000 a month.
  2. Add your sending domain in Resend (a subdomain such as keeps your main domain's reputation apart) and add the records it shows at your DNS provider: MX and TXT (SPF) on , TXT (DKIM) on . Add DMARC on the main domain.
  3. Create an API key with sending access, limited to that domain, and put it in the environment.
  4. Install the plugin, register it as a plugin and as the email provider with the same options (with Medusa's local provider kept on ), run the migrations.
  5. Set the branding and point the links at your storefront, the password reset page included.
  6. Send a test of the order confirmation to yourself; look at it on a phone and in dark mode.
  7. Choose the templates: turn off what another system sends, turn on the optional ones you want.
  8. Place and ship a real order; watch the Failed counter for the first days. Choose a Resend plan that covers your busiest day before it comes.

Security and data

  • The API key travels only in the header to . It is never sent to the admin (which sees "set" or "missing"), and it is masked, with every key, value and long token, in logs, stored errors and admin screens.
  • Password reset links never sit in a table. The reset e-mail does not go through Medusa's notification module, whose table keeps the data of every notification and returns it from to every admin user and secret API key; the plugin hands it straight to its provider (the same send log, switch and idempotency key). The send log keeps a hash of the token in the key, never the token; the simulated outbox of demo mode keeps the message with the link hidden, and demo mode handles no reset of an admin user. Without the provider registered in the process, a reset is not sent at all.
  • At most (3) reset e-mails per address in an hour, counted in the send log under a lock per address, so several processes never pass the limit together.
  • Addresses are masked in logs and in the send log (). Next to the masked address the log keeps a one-way hash of the address (to find the e-mails of one address and to count resets) and the Medusa customer id. It keeps the template, the subject, the status, the Resend id and ids of the order or customer; never the message body (except in demo mode, for the simulated outbox, with secret fields hidden).
  • Escaping by construction: every value from customers, the catalog and the configuration is escaped in the HTML; links are used only when they are absolute http(s) addresses; colours and fonts from the options are validated before they reach CSS. The admin preview runs in a sandboxed frame without scripts.
  • A stranger's words stay out of the welcome. Anyone can register with any address, and Medusa does not confirm it: the welcome shows a name only when it reads like one (letters, spaces, apostrophes, hyphens), a company only when it is not a link, an address or a domain, and the subject carries no name at all.
  • Previews and tests never carry a customer's details: with your newest order they keep the products, amounts and the order number, and replace the name, company, address, the order and cart ids and the tracking numbers with sample ones (a real cart id opens the cart, with the shopper's address, through the Store API).
  • Test sends are limited (5 per person in 10 minutes, 30 an hour for the store), counted in the send log as well as in memory, so a restart or a second instance does not reset them, and logged with who sent them.
  • The demo outbox shows the sample person: the store's products, amounts and numbers with the name, address and tracking numbers of the samples, so a demo on a copy of a live database never keeps customers' details.
  • Writes need the admin. Writes to (settings, test sends, retries, the demo seed) take a JSON body or the header and answer 415 otherwise: Medusa's session cookie is in production, so a form on another site could otherwise post with it. The plugin's admin sends both.
  • Shopper metadata is not state. A shopper may not set the keys of on a cart or the account through the Store API (400 ): Medusa copies cart metadata to the order, and such an order gets no e-mail. The language keys (, ) stay the shopper's own.
  • Reads do not write: routes only read; the demo outbox is built by (the page asks for it) and by the hourly housekeeping job.
  • Errors: unexpected server errors answer a plain sentence; the details stay in the server log, masked.
  • No retry storms: only temporary answers are tried again, at most twice, with the same idempotency key; a breaker stops retrying during an outage; quotas, auth and validation errors are final. A message whose fate is unknown is never resent by itself.
  • When the settings cannot be read, the last settings this process read stay in force; with none, nothing is sent and the message is logged as failed () for a person to retry. A template switched off in the admin never comes back on by itself.
  • Demo negotiations ( in the event) never e-mail anyone outside demo mode.
  • What Medusa itself keeps: the notification table of Medusa keeps the address and the data of every notification (names, order lines, links) with no end date; the retention of this plugin covers only its own send log. Messages the plugin skips (a template switched off, no API key, a duplicate) are there, without an external id: the send log of this page tells what really happened.
  • The send log deletes its own rows after ; nothing in Medusa is changed by this plugin (no cart or order metadata is written).

What this plugin does not do

  • It does not send staff notifications (a new order for the team), newsletters or marketing campaigns; the abandoned cart reminder is the only one, off by default, and it adds no unsubscribe link: check your consent basis before you turn it on.
  • It does not attach invoices or other files by itself; attachments passed to the notification module are sent along.
  • It does not read Resend's delivery, open or bounce events (webhooks): the log says what Resend accepted, not what reached the inbox.
  • It does not manage Resend domains, API keys or audiences.
  • It does not send SMS or push messages, only the email channel.
  • It does not translate your custom templates: they render what you write, in the languages you write.

Works with Koda Plus hosts

An app that shows every Koda Plus plugin in one place (like medusa.koda.plus) reads E-mails through the shared contract and never needs to know its tables:

  • : the manifest (mode, configuration, problems such as no API key or a provider that is not registered, the widget).
  • (or , up to 50; too): one line per record, the worst message speaking. A message that failed is red; one that may not have gone out, or an address that was refused (customers), is orange; one being sent is blue; sent is green, with the number of messages and the newest one. Messages skipped on purpose (a template switched off, no API key) only speak when there is nothing else, and test sends never count. A customer's line covers the customer's own e-mails and those of orders placed as a guest with the same address (found by the hash of the address). No facts: e-mails are not a payment, a delivery or a document.
  • : counters of the last 7 days, (red, orders: messages of orders that failed or may not have gone out, linked to ) and (orange, customers: addresses refused when a message was sent, linked to ). The plugin reads no bounce webhooks of Resend, so a message that bounced after Resend accepted it is not counted.
  • The order card registers as for the zone (tab order 80); a host that claims the zone shows it as a tab () and Medusa's own spot stays empty. Without a host nothing changes.
  • The page opens on deep links: (, , , , , ), (, , ), , and .

All three routes only read the send log (and, for a customer, the customer's address by id); they never seed the demo outbox, call Resend or write.

Public API

What other code may import: (your own templates: , , the kit, , the data types), (, , , ), (the module, its options and the same template API), (the notification provider) and . Type declarations ship with the package, so is checked in a strict TypeScript app. Every other path is internal and may change in any release.

Uninstall

  1. Remove both registrations from : the plugin in , and its provider in the notification module. Put back a provider for the channel if the store still sends e-mails, and keep the local provider on (or remove the whole entry to go back to Medusa's defaults).
  2. Remove the package from .
  3. The tables and stay with their data. Drop them by hand when you no longer need the log, or run before removing the package. Medusa keeps its own notification rows and the record of the migrations; the row of its provider table is disabled by Medusa once the provider is gone.

Compatibility

Medusa 2.12 to 2.21 (peer range ) and Node.js 20+. The tests and the build run on Medusa 2.15.3; the release of each version runs the smoke test on a fresh Medusa 2.12.6 and 2.21.2 app (migrations, build, start and the admin pages). Admin translations need Medusa 2.12 or newer. Developing the plugin needs Node.js 22.6+ (the tests run the TypeScript sources directly).

Development

covers every built-in template in both languages (with full and with almost no data, escaping, the clipping size, dark mode, the text part), the delivery path of the provider (dev mode, demo mode, the exact Resend request, idempotency, switches, finished content, failures, a person's retry, a missing table), the Resend client (error mapping, retries with the same key, retry-after, the breaker, pacing), options, links, locales and formats, masking, keys, the registry and app templates, settings, the SQL of the send log, the mappers from Medusa records, and the flows with a fake Medusa container. Tests need Node.js 22.6 or newer (they run the TypeScript sources directly). What the plugin relies on from the Resend and Medusa documentation is in docs/resend-api-notes.md. To try the plugin in a Medusa app, run here, then in the app.

Commercial support

Built and maintained by Koda Plus, a Medusa agency from Poland. The templates grew out of the e-mails of our Medusa demo store and the stores we build. Need your own templates, another provider or a Medusa store? Write to kontakt@koda.plus.

Trademarks

Resend is a trademark of its owner, used here only to identify the e-mail service this plugin sends through. This is an independent integration built on the public Resend API, not affiliated with or endorsed by Resend.

License

MIT, see LICENSE.

Changelog

Every release is in CHANGELOG.md. Version 0.2.0 keeps password reset links out of every table, limits resets per address, adds the contract for Koda Plus hosts (summaries of orders and customers, board counters, the order card as a tab) and ships type declarations; it needs .

Еще в этой категории

Посмотреть все
Уведомления
Nodemailer logo

Nodemailer

От Perseides

Отправляйте email-уведомления через Nodemailer (SMTP)

Загрузка данных
npm
Уведомления
Mailgun logo

Mailgun

От Webbers

Отправляйте и управляйте уведомлениями по электронной почте

Загрузка данных
GitHubnpm
Уведомления
Postmark logo

Postmark

От Bram-hammer

Транзакционные письма через Postmark

Загрузка данных
GitHubnpm

Еще от этого автора

Посмотреть все
Доставка
I

Inpost

От Koda-plus

Провайдер фулфилмента InPost от Koda Plus для Medusa на базе InPost ShipX API. Поддерживает постаматы Paczkomat и курьерскую доставку InPost с наложенным платежом и предлагает четыре опции. Отправления создаются только по плану, который вы сначала просматриваете (по умолчанию выключено).

Загрузка данных
GitHubnpm
Другое
V

Vat whitelist

От Koda-plus

VAT Whitelist от Koda Plus для Medusa проверяет польский NIP по белому списку Министерства финансов (biala lista), а номера VAT стран ЕС по VIES. Среди возможностей статус VAT, банковские счета для раздельного платежа (split payment), список контрагентов с повторными проверками и история проверок.

Загрузка данных
GitHubnpm
Другое
O

Olx

От Koda-plus

Плагин Koda Plus для Medusa привязывает объявления OLX к товарам по SKU, оповещает о расхождениях между OLX и магазином, показывает статистику объявлений и непрочитанные сообщения. Операции записи включаются отдельно. Они снимают объявления с публикации и возвращают их по остаткам, обновляют цены и публикуют товары.

Загрузка данных
GitHubnpm
npm install @koda-plus/medusa-plugin-emails
1import { defineConfig } from "@medusajs/framework/utils"
2
3const emails = {
4 apiKey: process.env.RESEND_API_KEY,
5 from: process.env.EMAILS_FROM, // "Your Store <orders@mail.example.com>"
6 replyTo: "support@example.com",
7 defaultLocale: "en", // or "pl"
8 timeZone: "Europe/Warsaw",
9 storefrontUrl: "https://shop.example.com",
10 brand: {
11 name: "Your Store",
12 accentColor: "#26D07C",
13 footer: { en: "Your Store Ltd, 1 Market Street, London", pl: "Your Store sp. z o.o., ul. Rynek 1, Warszawa" },
14 supportEmail: "support@example.com",
15 },
16 demo: process.env.EMAILS_DEMO === "true", // a simulated outbox, nothing leaves the server
17}
18
19module.exports = defineConfig({
20 // ...
21 plugins: [{ resolve: "@koda-plus/medusa-plugin-emails", options: emails }],
22 modules: [
23 {
24 resolve: "@medusajs/medusa/notification",
25 options: {
26 providers: [
27 {
28 resolve: "@koda-plus/medusa-plugin-emails/providers/emails",
29 id: "emails",
30 options: { channels: ["email"], ...emails },
31 },
32 {
33 // Keep Medusa's own provider on the feed channel: product import and export
34 // and the order export send their notifications there, and fail without it.
35 resolve: "@medusajs/medusa/notification-local",
36 id: "local",
37 options: { name: "Local Notification Provider", channels: ["feed"] },
38 },
39 ],
40 },
41 },
42 ],
43})
1RESEND_API_KEY=re_...
2EMAILS_FROM="Your Store <orders@mail.example.com>"
3# EMAILS_DEMO=true # only on a demo: a simulated outbox instead of Resend
1import { defineEmailTemplate } from "@koda-plus/medusa-plugin-emails/templates"
2
3export const companyApproved = defineEmailTemplate<{ company_name?: string; tier?: string }>({
4 label: { en: "B2B company approved", pl: "Firma B2B zatwierdzona" },
5 description: { en: "The team approves a company account", pl: "Zespół zatwierdza konto firmy" },
6 trigger: { kind: "event", name: "company.approved" },
7 sample: { company_name: "Krawczyk Builders Ltd", tier: "B2B Premium" },
8 render: ({ data, locale, kit, links }) => ({
9 subject: locale === "pl" ? `Ceny ${data.tier} są już aktywne` : `Your ${data.tier} prices are live`,
10 eyebrow: locale === "pl" ? "Firma zweryfikowana" : "Company verified",
11 title: [locale === "pl" ? "Gotowe. Widzisz już " : "Done. You now see ", kit.accent(data.tier ?? ""), "."],
12 band: kit.card({ title: data.company_name ?? "", status: { label: "OK", tone: "accent" } }),
13 blocks: [kit.actions({ label: locale === "pl" ? "Zobacz ceny" : "See your prices", href: links.store() })],
14 }),
15})
16
17// medusa-config.ts: const emails = { ..., templates: { "company.approved": companyApproved } }
1import { sendEmailWorkflow } from "@koda-plus/medusa-plugin-emails/workflows"
2
3await sendEmailWorkflow(container).run({
4 input: { template: "company.approved", to: customer.email, data: { company_name, tier }, idempotencyKey: `company.approved:${company.id}` },
5})
1npm install
2npm test
3npm run typecheck
4npm run build