Skip to content

Email Notifications

Use the built-in console adapter during development. It logs every email to your terminal — no API key, no SMTP server, no external service needed.

commerce.config.ts
import { consoleEmailAdapter } from "@porulle/core";
export default defineConfig({
email: consoleEmailAdapter(),
});

When an email is triggered, you see output like:

============================================================
EMAIL: order-confirmation
============================================================
To: customer@example.com
Template: order-confirmation
Data:
orderId: "ord_abc123"
total: 13997
currency: "USD"
============================================================
Terminal window
bun add @porulle/adapter-resend
commerce.config.ts
import { resendAdapter } from "@porulle/adapter-resend";
export default defineConfig({
email: resendAdapter({
apiKey: process.env.RESEND_API_KEY!,
from: "Acme Store <orders@acme.com>",
}),
});
Terminal window
bun add @porulle/adapter-ses
commerce.config.ts
import { sesAdapter } from "@porulle/adapter-ses";
export default defineConfig({
email: sesAdapter({
region: "us-east-1",
from: "Acme Store <orders@acme.com>",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
});

The sender address must be verified in SES. If your account is in the SES sandbox, recipient addresses must also be verified.

Both adapters ship with default HTML templates for all built-in email types. No additional configuration is needed for basic use.

Override subjects and HTML bodies per template key:

email: resendAdapter({
apiKey: process.env.RESEND_API_KEY!,
from: "Acme Store <orders@acme.com>",
subjects: {
"order-confirmation": (data) => `Thanks for your order #${String(data.orderId).slice(0, 8)}!`,
},
templates: {
"order-confirmation": (data) => `
<h1>Order Confirmed</h1>
<p>Hi! Your order <strong>#${data.orderId}</strong> is confirmed.</p>
<p>Total: ${data.currency} ${(Number(data.total) / 100).toFixed(2)}</p>
<a href="https://mystore.com/orders/${data.orderId}">View Order</a>
`,
},
}),

If you use Resend’s dashboard templates, pass template IDs:

email: resendAdapter({
apiKey: process.env.RESEND_API_KEY!,
from: "Acme Store <orders@acme.com>",
resendTemplateIds: {
"order-confirmation": "tmpl_abc123",
"password-reset": "tmpl_def456",
},
}),
TemplateTriggered byData fields
order-confirmationCheckout completionorderId, total, currency
order-status-changeOrder status hookorderId, newStatus, previousStatus
password-resetBetter Auth password reseturl
email-verificationBetter Auth email verificationurl
appointment:reminderAppointment plugin (24h + 1h before)bookingId, reminderType
appointment:confirmation-noticeAppointment booking confirmedbookingId, providerId
appointment:cancellation-noticeAppointment booking cancelledbookingId
appointment:no-show-noticeAppointment marked no-showbookingId

The email interface requires a single send method:

interface EmailAdapter {
send(input: {
template: string;
to: string;
data?: Record<string, unknown>;
}): Promise<void>;
}

Any object matching this signature works. Example SendGrid adapter:

import sgMail from "@sendgrid/mail";
export function sendgridAdapter(options: { apiKey: string; from: string }) {
sgMail.setApiKey(options.apiKey);
return {
async send(input: { template: string; to: string; data?: Record<string, unknown> }) {
await sgMail.send({
to: input.to,
from: options.from,
subject: input.template,
html: `<pre>${JSON.stringify(input.data, null, 2)}</pre>`,
});
},
};
}