Skip to content

Email Service

Applications can send emails through the core EmailManager system using the context.email service available in server functions. This allows plugins to send templated emails (e.g. welcome messages, approval notifications) without direct access to the platform schema or the EmailManager class.

The feature has two parts:

  1. Declarative email template registration — JSON files in the email-templates/ folder are automatically registered during installation
  2. Email sending via context.email — server functions call context.email.send() to send emails using registered templates

Required permission: notifications in manifest.json.

manifest.json
{
"id": "my-app",
"permissions": ["database", "remote_functions", "notifications"]
}
email-templates/welcome.json
{
"name": "Welcome Email",
"locales": {
"en": {
"subject": "Welcome to the system!",
"html": "<h1>Hello {{name}}!</h1><p>Your account has been created.</p>",
"text": "Hello {{name}}! Your account has been created."
},
"hu": {
"subject": "Üdvözöljük a rendszerben!",
"html": "<h1>Kedves {{name}}!</h1><p>Fiókja létrejött.</p>",
"text": "Kedves {{name}}! Fiókja létrejött."
}
},
"requiredData": ["name", "email"],
"optionalData": ["position"]
}
server/functions.ts
export async function createUser(params, context) {
// ... create user logic ...
// Send welcome email
const result = await context.email.send({
to: params.email,
template: 'welcome', // Just the template name — no prefix needed
data: { name: params.name, email: params.email },
locale: 'en'
});
if (!result.success) {
console.warn('Email sending failed:', result.error);
}
return { success: true };
}

The email property is available on the context object in server functions when the application has the notifications permission.

Sends a templated email through the core EmailManager.

ParameterTypeRequiredDescription
tostring | string[]YesRecipient email address(es)
templatestringYesTemplate name (without app ID prefix)
dataRecord<string, unknown>YesTemplate variables
localestringNoLocale code (default: 'hu')

Returns: Promise<{ success: boolean; messageId?: string; error?: string }>

const result = await context.email.send({
to: 'user@example.com',
template: 'order_confirmation',
data: { orderId: 1234, total: '€99.00' },
locale: 'en'
});

The template parameter is automatically prefixed with the application ID. You only need to provide the template name as defined in your email-templates/ folder:

You writeWhat gets resolved
'welcome''my-app:welcome'
'order_confirmation''my-app:order_confirmation'

This means you never need to know or use the full prefixed name in your code.

If the application does not have the notifications permission, context.email is undefined. Always check before using:

if (context.email) {
await context.email.send({ /* ... */ });
}

Or handle it gracefully:

export async function sendNotification(params, context) {
if (!context.email) {
throw new Error('Email service is not available — check notifications permission');
}
// ...
}

Templates are JSON files in the email-templates/ directory of your application.

my-app/
├── email-templates/
│ ├── welcome.json
│ ├── order_confirmation.json
│ └── password_reset.json
├── manifest.json
└── ...
{
"name": "Human-readable template name",
"locales": {
"en": {
"subject": "Email subject with {{variable}} support",
"html": "<h1>HTML body with {{variable}} support</h1>",
"text": "Plain text body with {{variable}} support"
},
"hu": {
"subject": "Email tárgya {{variable}} támogatással",
"html": "<h1>HTML törzs {{variable}} támogatással</h1>",
"text": "Szöveges törzs {{variable}} támogatással"
}
},
"requiredData": ["variable"],
"optionalData": ["optionalVariable"]
}
FieldTypeDescription
namestringDisplay name for the template
localesRecord<string, LocaleData>Locale-specific content (subject, html, text)
requiredDatastring[]Required template variables
optionalDatastring[]Optional template variables

Each locale entry contains:

FieldTypeDescription
subjectstringEmail subject line (supports {{variable}} syntax)
htmlstringHTML email body
textstringPlain text fallback body

Use {{variableName}} syntax in subject, html, and text fields. Variables are replaced with values from the data parameter when sending.

When a plugin is installed, the core PluginInstaller automatically:

  1. Reads all .json files from the email-templates/ directory
  2. For each file and each locale, creates a row in platform.email_templates
  3. The type column is set to {appId}:{fileName} (e.g. my-app:welcome)
  4. Uses upsert (ON CONFLICT DO UPDATE) — reinstalling updates existing templates

When a plugin is removed, all email template records with the {appId}:% prefix are deleted from platform.email_templates.

Reinstalling or updating a plugin re-registers all templates using upsert. Changed templates are updated, new ones are added, but templates that were removed from the email-templates/ folder are not automatically deleted — they remain in the database until the plugin is fully uninstalled.

Email sending failures do not throw exceptions. Instead, context.email.send() returns an error object:

const result = await context.email.send({
to: 'user@example.com',
template: 'welcome',
data: { name: 'John' }
});
if (!result.success) {
// Log the error, show a toast, or ignore
console.error('Email failed:', result.error);
// The calling function decides how to handle it
}
server/functions.ts
export async function createEmployeeWithUser(params, context) {
const { db, email } = context;
const { name, emailAddress, position, department } = params;
// 1. Create user and employee in a transaction
const employee = await db.execute(`
-- ... insert logic ...
`);
// 2. Send welcome email (non-blocking, non-critical)
if (email) {
const emailResult = await email.send({
to: emailAddress,
template: 'employee_welcome',
data: { name, email: emailAddress, position, department },
locale: 'hu'
});
if (!emailResult.success) {
console.warn(`Welcome email failed for ${emailAddress}: ${emailResult.error}`);
}
}
return { employee: employee.rows[0] };
}