Confirm a process is still alive, and if it isn't, send an email about it.
This works with any SMTP provider, Mandrill, Resend, or SendGrid.
stillalive can be used two ways (see Usage): embedded in your own Node app as a library, or run directly as a CLI. Install accordingly:
| How you'll use it | Install |
|---|---|
| As a library (import it in your code) | npm install stillalive |
As a CLI (run the stillalive command) |
npm install -g stillalive |
Resend and SendGrid additionally need their official SDK, which ships as an optional peer dependency so it only gets installed if you actually use it (SMTP and Mandrill need nothing extra):
| Provider | Extra install |
|---|---|
| SMTP / Mandrill | none |
| Resend | npm install resend |
| SendGrid | npm install @sendgrid/mail |
The SDK is loaded via a dynamic import() only when its service is selected, so the package works fine with neither installed. If you configure resend or sendgrid without installing its SDK, stillalive fails fast at startup with a clear error telling you the exact npm install command to run.
Both strategies take the same two pieces of configuration: a key (a shared secret callers must present) and a provider config object (see Configuring an email provider).
import stillalive from 'stillalive' returns an async factory: await stillalive(key, provider, port). It starts an Express server and returns the app, so you can add your own routes:
import stillalive from 'stillalive';
import config from './config.emailProvider.json' with { type: 'json' };
const port = process.env.PORT || 8080;
const app = await stillalive(config.key, config.provider, port);
app.get('/health', (_req, res) => {
res.status(200).send('OK');
});port is optional and defaults to process.env.PORT, then 3000.
Point the stillalive command at a JSON config file:
stillalive ./path/to/config.json [port]
port is optional and defaults to process.env.PORT, then 3000. The config file holds the key and provider config (the provider object goes under provider):
{
"key": "my-secret-key",
"provider": {
"service": "smtp-mail.outlook.com",
"auth": {
"user": "user@example.com",
"pass": "insert_password_here"
}
}
}The provider config object selects the email service via its service field. The examples below show it under the provider key of a CLI config file; when used as a library, pass the inner object as the second argument to stillalive(key, provider, port). Ready-to-copy config files for each provider live in the examples/ folder.
If using SMTP, name your SMTP host as the service (see examples/config.smtp.json):
{
"key": "my-secret-key",
"provider": {
"service": "smtp-mail.outlook.com",
"auth": {
"user": "user@example.com",
"pass": "insert_password_here"
}
}
}If using Mandrill (see examples/config.mandrill.json):
{
"key": "my-secret-key",
"provider": {
"service": "mandrill",
"apiKey": "md-EgWVMWEjZF2KdSlocGs2Aw"
}
}If using Resend (requires npm install resend; see examples/config.resend.json):
{
"key": "my-secret-key",
"provider": {
"service": "resend",
"apiKey": "re_xxxxxxxxxxxx"
}
}If using SendGrid (requires npm install @sendgrid/mail; see examples/config.sendgrid.json):
{
"key": "my-secret-key",
"provider": {
"service": "sendgrid",
"apiKey": "SG.xxxxxxxxxxxx"
}
}Whatever provider you configure, requests use the same canonical email object (see email object below) -- stillalive maps it to each provider's native format for you.
send a put to host/still/alive/:id where id is your app specific timeout's name
The body of your request should be json as follows. The email object uses a single canonical shape that works the same no matter which provider you've configured -- stillalive maps it internally to SMTP, Mandrill, Resend or SendGrid:
{
"key": "server key (set in your config file)",
"email": {
"from": "you@domain.tld",
"to": "name@domain.tld",
"subject": "subject line",
"text": "text body of email"
},
"interval": {
"minutes": 5
}
}Every address field (from, to, cc, bcc, replyTo) accepts any of:
- a string:
"name@domain.tld" - a string with a display name:
"Their Name <name@domain.tld>" - an object:
{ "email": "name@domain.tld", "name": "Their Name" } - an array of any of the above (for
to/cc/bcc)
Provide text, html, or both. A fuller example:
{
"from": { "email": "you@domain.tld", "name": "You" },
"to": [
"first@domain.tld",
{ "email": "second@domain.tld", "name": "Second Person" }
],
"cc": "cc@domain.tld",
"bcc": "bcc@domain.tld",
"replyTo": "reply@domain.tld",
"subject": "subject line",
"text": "text body of email",
"html": "<p>html body of email</p>"
}The interval field accepts a number of milliseconds, or an object with any of weeks, days, hours, minutes, seconds, and milliseconds (which are summed). For example, { "minutes": 5 } or { "hours": 1, "minutes": 30 }.
Arming a timer (PUT /still/alive/:id) validates the request and responds with 400 and a JSON body when something is wrong:
| Condition | Response |
|---|---|
Missing or incorrect key |
{ "error": "bad request" } |
Invalid email payload |
{ "error": "invalid email", "details": [ ... ] } |
Missing or invalid interval |
{ "error": "invalid interval" } |
The details array lists every problem found in the email object, for example:
{
"error": "invalid email",
"details": [
"`from` must be a valid email address",
"`to` must include at least one recipient",
"`subject` must be a non-empty string",
"either `text` or `html` body is required"
]
}A valid payload needs a syntactically valid from, at least one valid to, a non-empty subject, and a text or html body. Provider config is validated when the server starts: a missing/invalid key, an unknown provider shape, a missing API apiKey, or missing SMTP auth.user/auth.pass throws a clear TypeError at startup.
Send a POST to host/active with the server key to list every currently-armed timer. The key is checked the same way as the other routes -- an incorrect key responds with 400 { "error": "bad request" }.
{ "key": "server key (set in your config file)" }The response lists each active timer by id, when it will fire (expiresAt, ISO 8601), and how long until it does (msRemaining):
{
"active": [
{ "id": "nightly-export", "expiresAt": "2026-06-23T20:29:14.509Z", "msRemaining": 599984 }
]
}v3 removes the legacy aliases that earlier versions silently accepted, in favor of a single canonical shape. Update any of the following:
| Removed (pre-v3) | Use instead |
|---|---|
from_email / from_name on the email object |
from (a string or { email, name }) |
reply_to (snake_case) |
replyTo |
address key on an address object |
email |
Mandrill-style to entries with a type field |
a string or { email, name } |
accessKeyId in provider config |
apiKey |
smtp / api keys in a CLI config file |
provider |
In addition, requests and config are now validated up front (see validation and error responses). Payloads or configs that previously "worked" but were incomplete will now be rejected with an explicit error instead of failing silently.