Email and runtime services
View as MarkdownGenerated Sapporta projects construct long-lived runtime services during boot
and pass them into loadApp(). The mailer is the built-in example: Nodemailer
configuration stays at the process boundary while routes receive a small typed
service.
This guide shows how to send development email, inject the mailer into a domain workflow, and switch to SMTP in production. The same pattern works for queues, object storage, or external API clients that need explicit construction and cleanup.
Add an email to <workflow> using the generated Sapporta mailer. Keep transportconfiguration in boot, inject the service into domain code, and show the fullmessage with stream transport before configuring production delivery.Keep construction at boot
Section titled “Keep construction at boot”readProjectAuthEnv() parses the server environment. createSapportaMailer()
constructs one Nodemailer transport, default sender, and sendMail() helper.
boot.ts passes that service into the application entry point:
const projectEnv = readProjectAuthEnv();const mailer = createSapportaMailer(projectEnv.mail);
const apiApp = new TsRestApi<SapportaEnv>();loadApp(apiApp, { conn, mailer });The route or domain service accepts the capability it uses instead of importing environment parsing or the boot module:
import type { SapportaMailer } from "../../mailer.js";
export interface CompletionNoticeInput { recipient: string; taskTitle: string;}
export async function sendCompletionNotice( mailer: Pick<SapportaMailer, "sendMail">, input: CompletionNoticeInput,) { await mailer.sendMail({ to: input.recipient, subject: `Completed: ${input.taskTitle}`, text: `The task "${input.taskTitle}" is complete.`, });}Call the mail service after the database transaction commits. Nodemailer is an external effect and should not run inside the synchronous SQLite transaction callback. If delivery must be durable, insert an outbox record in the transaction and process it separately.
Inspect mail locally
Section titled “Inspect mail locally”Stream transport does not deliver the message. It writes the complete generated email to the API console, including sender, recipient, subject, text, and HTML.
SAPPORTA_MAIL_TRANSPORT=streamSAPPORTA_MAIL_FROM=Task App <no-reply@example.com>pnpm dev# Trigger the workflow from the browser or its app-owned endpoint.pnpm exec sapporta api post /api/tasks/1/complete --body '{}'Production uses SAPPORTA_MAIL_TRANSPORT=smtp with either SMTP_URL or the
individual SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, and SMTP_PASS
values. disabled is an explicit production choice when the app should skip
delivery.
The workflow now depends on a typed mail capability rather than process-global configuration. The non-obvious boundary is transaction timing: commit domain state before sending, or use an outbox when state and eventual delivery must be coordinated. Continue with custom endpoints or application configuration.