Email & Templates
FerrisKey sends transactional email for password resets, magic link sign-in, and email verification. Delivery is configured per realm, and each email type can be pointed at a template of your own.
Configure SMTP
SMTP settings live in the database, on the realm, not in a global environment variable. Two realms in the same deployment can use completely different mail providers.
No global SMTP
SMTP is configured per realm, not globally. See the Configuration guide for the short overview. This page covers the full field reference and API.
SMTP fields
| Field | Type | Notes |
|---|---|---|
host | String | SMTP server hostname |
port | u16 | Port number (1–65535) |
username | String | SMTP authentication username |
password | String | SMTP authentication password. Write-only: the API never returns it |
from_email | String | Sender address, must be a valid email |
from_name | String | Sender display name |
encryption | Enum | tls | starttls | none |
Encryption modes:
| Mode | Port | When to use |
|---|---|---|
tls | 465 | Implicit TLS from the first byte. What most modern providers want |
starttls | 587 | Plain connection upgraded to TLS. Common on corporate relays |
none | 25 / any | No encryption. Only on a trusted local network, or for testing |
SMTP API endpoints
All endpoints are scoped to a realm and require authentication.
| Method | Path | Description |
|---|---|---|
GET | /realms/{realm_name}/smtp-config | Read the current config (password omitted) |
PUT | /realms/{realm_name}/smtp-config | Create or replace the config |
DELETE | /realms/{realm_name}/smtp-config | Remove the config |
Required permissions: reading the SMTP config requires view access to the realm. Creating, updating, or deleting it requires ManageRealm. There are no SMTP-specific permissions beyond the realm gates.
Configure SMTP in the console
Open Realm Settings
In the left sidebar, select the realm you want to configure. Navigate to Realm Settings.
Go to the Email tab
Click the Email tab. You will see the SMTP configuration form.
Fill in the provider details
Enter the host, port, credentials, sender address and name. Select the encryption mode that matches your provider.
Common provider settings:
| Provider | Host | Port | Encryption |
|---|---|---|---|
| Gmail (App Password) | smtp.gmail.com | 465 | tls |
| Mailgun | smtp.mailgun.org | 587 | starttls |
| Resend | smtp.resend.com | 465 | tls |
| Local (MailHog / MailPit) | localhost | 1025 | none |
Save and verify
Click Save. Send a test email from the console to confirm delivery reaches the inbox before enabling email-gated features.
Password is write-only
The API never returns the SMTP password. If you need to rotate credentials, submit a full PUT with the new password included.
Transactional emails
There are three types of transactional email, each gated by a realm toggle.
| Email type | Identifier | Sent when | Realm toggle required |
|---|---|---|---|
| Password reset | reset_password | A user requests a password reset link | forgot_password_enabled |
| Magic link | magic_link | A user requests passwordless email login | magic_link_enabled |
| Email verification | email_verification | A user must verify their email address | email_verification_enabled |
Turning a toggle on in Realm Settings activates the matching flow. Until you assign a template of your own, FerrisKey uses the built-in default.
Customizing templates
Template engine
Templates are rendered by a small interpolation engine written for the purpose, not Handlebars or Tera. Placeholders use double braces:
{{variable_name}}
Every interpolated value is HTML-escaped before insertion (&, <, >, ", '), so a template cannot be turned into an injection vector.
HTML escaping
Values inserted via {{...}} are always HTML-escaped. If a user’s name is Alice <script>, it renders as Alice <script> in the final email, safe to embed in HTML.
Templates are stored as a JSON structure that FerrisKey renders to MJML and then to HTML before sending.
Available variables
These variables are available in every template:
| Variable | Description |
|---|---|
user.first_name | User’s first name |
user.last_name | User’s last name |
user.email | User’s email address |
expiration | Expiry time of the token or link |
Each email type also provides one link variable:
| Email type | Link variable | Description |
|---|---|---|
reset_password | reset_link | The password-reset URL |
magic_link | magic_link | The passwordless login URL |
email_verification | verification_link | The email verification URL |
You can query the available variables for any type without authentication:
GET /email-templates/variables/{email_type}
email_type must be one of: reset_password, magic_link, email_verification.
Example response for reset_password:
{
"data": [
{ "name": "user.first_name", "description": "User's first name" },
{ "name": "user.last_name", "description": "User's last name" },
{ "name": "user.email", "description": "User's email address" },
{ "name": "expiration", "description": "Expiration time" },
{ "name": "reset_link", "description": "Password reset link" }
]
}
Template API endpoints
| Method | Path | Description |
|---|---|---|
GET | /realms/{realm_name}/email-templates | List all templates for the realm |
POST | /realms/{realm_name}/email-templates | Create a new template |
GET | /realms/{realm_name}/email-templates/{template_id} | Get a template by ID |
PUT | /realms/{realm_name}/email-templates/{template_id} | Update a template |
DELETE | /realms/{realm_name}/email-templates/{template_id} | Delete a template |
Request body for POST: name, email_type, structure.
Request body for PUT: name, structure (email_type cannot be changed after creation).
Required permissions: ViewEmailTemplates to read. ManageEmailTemplates to create, update, or delete. ManageRealm also grants both.
Linking a template to an email type
Creating a template does not activate it. Point the realm at it by updating realm settings:
| Realm setting field | Applies to |
|---|---|
reset_password_template_id | reset_password emails |
magic_link_template_id | magic_link emails |
email_verification_template_id | email_verification emails |
Set the field to the template UUID to activate it. Set it to null (or leave it unset) to fall back to the built-in default.
Example
Template snippet
A minimal password-reset template body using placeholder syntax:
<p>Hi {{user.first_name}},</p>
<p>
Someone requested a password reset for your account (<strong>{{user.email}}</strong>).
This link expires in {{expiration}}.
</p>
<p>
<a href="{{reset_link}}">Reset your password</a>
</p>
<p>If you did not request this, you can safely ignore this email.</p>
Variables response
{
"data": [
{ "name": "user.first_name", "description": "User's first name" },
{ "name": "user.last_name", "description": "User's last name" },
{ "name": "user.email", "description": "User's email address" },
{ "name": "expiration", "description": "Expiration time" },
{ "name": "magic_link", "description": "Magic link URL" }
]
}