stillalive
v3.0.0
Published
Dead man's switch email alert server
Readme
still alive
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.
Installation
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.
Usage
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).
As a library
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.
As a CLI
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": "[email protected]",
"pass": "insert_password_here"
}
}
}Configuring an email provider
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": "[email protected]",
"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.
usage
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": "[email protected]",
"to": "[email protected]",
"subject": "subject line",
"text": "text body of email"
},
"interval": {
"minutes": 5
}
}email object
Every address field (from, to, cc, bcc, replyTo) accepts any of:
- a string:
"[email protected]" - a string with a display name:
"Their Name <[email protected]>" - an object:
{ "email": "[email protected]", "name": "Their Name" } - an array of any of the above (for
to/cc/bcc)
Provide text, html, or both. A fuller example:
{
"from": { "email": "[email protected]", "name": "You" },
"to": [
"[email protected]",
{ "email": "[email protected]", "name": "Second Person" }
],
"cc": "[email protected]",
"bcc": "[email protected]",
"replyTo": "[email protected]",
"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 }.
validation and error responses
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.
listing active timers
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 }
]
}Migrating to v3
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.
