A self-hosted dead man's switch. Stop checking in, and it delivers pre-written messages — and encrypted files — to the people you chose.
Stack: TypeScript · Fastify · PostgreSQL 16 · React (Vite + Tailwind) · pnpm monorepo
You set a check-in schedule. A background tick worker watches it. Miss your window past the grace period and the switch fires, emailing designated contacts with whatever you left for them.
The interesting problem is that the server has to hold data it must not be able to casually read. Contact details and attachments are encrypted at rest with AES-256-GCM envelope encryption — a master key wraps per-user data keys — so compromising the database alone doesn't yield plaintext. Registration is invite-only, and it's built for single-operator use behind a reverse proxy with automatic HTTPS.
pnpm install
pnpm build
pnpm testNeeds PostgreSQL 16 and a master encryption key; the full walkthrough is below.
Working end to end, with an operational runbook and a manual QA checklist. Deployment is documented for Windows Server behind Caddy.
pnpm monorepo
├── packages/shared # TypeScript types, timeline calculator
├── packages/api # Fastify REST API + background tick worker
└── packages/web # React SPA (Vite + Tailwind)
- Database: PostgreSQL 16
- Email: Resend SDK (or stub for dev)
- Encryption: AES-256-GCM with a master encryption key wrapping per-user data keys
- Reverse proxy: Caddy (automatic HTTPS)
healthy → due → strike(s) → final_warning → fired
↑
Any state can be → disarmed ────┘ (re-arm possible)
Check-in at any armed state resets to healthy. If the operator goes silent through all strikes, the system enters final_warning (hourly alerts to the operator), then fires (sends all contact emails).
Install these on the server:
- Node.js 20 LTS — https://nodejs.org/en/download/
- PostgreSQL 16 — https://www.postgresql.org/download/windows/
- pnpm — after Node is installed:
npm install -g pnpm - Caddy — https://caddyserver.com/download (place
caddy.exeon PATH or install as a Windows service)
Open psql as the postgres superuser:
CREATE USER dms_app WITH PASSWORD '<generate-a-strong-password>';
CREATE DATABASE deadmansswitch OWNER dms_app;
\c deadmansswitch
CREATE EXTENSION IF NOT EXISTS citext;cd C:\srv
git clone <repo-url> deadmansswitch
cd deadmansswitch
pnpm install
pnpm build# Generate the master encryption key
powershell -File scripts\generate-master-key.ps1WARNING: Back up
MASTER_ENCRYPTION_KEYimmediately. If lost, all encrypted data (contacts, attachments) is permanently unrecoverable.
Copy the template and fill in values:
copy .env.example packages\api\.env
notepad packages\api\.envRequired values:
| Variable | Description |
|---|---|
DATABASE_URL |
postgresql://dms_app:<password>@localhost:5432/deadmansswitch |
MASTER_ENCRYPTION_KEY |
64-char hex from generate-master-key.ps1 |
SESSION_SECRET |
Random string, at least 32 characters |
OPERATOR_EMAIL |
Your email for system alerts |
RESEND_API_KEY |
From https://resend.com (required for real emails) |
BASE_URL |
https://dmswitch.mitchelltrout.com |
cd C:\srv\deadmansswitch
pnpm --filter @dms/api db:migrateForeground (console stays open):
scripts\start.bat
The API starts on 127.0.0.1:3002.
Paste the contents of Caddyfile.snippet into your existing Caddyfile, then reload:
caddy reload --config C:\srv\caddy\CaddyfileThis serves the React SPA at dmswitch.mitchelltrout.com with automatic HTTPS and proxies /api/* to the Node server.
Copy the built web assets to the Caddy serve directory:
Copy-Item -Recurse -Force packages\web\dist\* C:\srv\deadmansswitch\web\Open the database:
INSERT INTO invites (id, code, expires_at, notes)
VALUES (gen_random_uuid(), 'my-first-invite', NOW() + interval '7 days', 'self');Then visit https://dmswitch.mitchelltrout.com/register and enter the invite code.
- Open Task Scheduler → Create Basic Task
- Name:
DeadMansSwitch - Trigger: When the computer starts
- Action: Start a program
- Program:
wscript.exe - Arguments:
"C:\srv\deadmansswitch\scripts\start-hidden.vbs"
- Program:
- Check Run whether user is logged on or not
- Check Run with highest privileges
cd C:\srv\deadmansswitch
# Stop the server (close console or kill node.exe)
git pull
pnpm install
pnpm build
pnpm --filter @dms/api db:migrate
# Re-deploy frontend
Copy-Item -Recurse -Force packages\web\dist\* C:\srv\deadmansswitch\web\
# Start the server
scripts\start.batRun the backup script:
powershell -File C:\srv\deadmansswitch\scripts\backup.ps1This creates a timestamped directory under C:\srv\deadmansswitch\data\backups\ containing:
db.sql— full database dumpattachments\— copy of all encrypted attachmentsenv.gpg— GPG-encrypted.env(ifBACKUP_GPG_RECIPIENTis set andgpgis on PATH)
Old backups are pruned after 30 days (configurable via -RetentionDays).
Schedule daily via Task Scheduler:
- Create Basic Task → Name:
DMS Backup - Trigger: Daily at your preferred time
- Action: Start a program
- Program:
powershell.exe - Arguments:
-File "C:\srv\deadmansswitch\scripts\backup.ps1"
- Program:
See docs/runbook.md for full restore instructions.
MASTER_ENCRYPTION_KEYis the single most critical secret in this system.
- It encrypts all contact details (names, emails, message bodies) and attachment files
- If lost, there is no recovery path — all encrypted data becomes permanently inaccessible
- Store a copy in a separate, secure location (password manager, offline USB, safety deposit box)
- Never commit it to version control
- The
.envfile containing it should be backed up separately (the backup script supports GPG encryption for this)
See docs/runbook.md for:
- Starting/stopping the server
- Viewing and tailing logs
- Minting invites
- Disabling users
- Rotating SESSION_SECRET
- Restoring from backup
- Responding to operator alerts
After deployment, work through docs/qa-checklist.md to verify all critical paths.
Server won't start:
- Check that
DATABASE_URLis correct and PostgreSQL is running - Verify
packages/api/.envexists and has all required variables - Check
MASTER_ENCRYPTION_KEYis exactly 64 hex characters
Can't reach the site:
- Verify Caddy is running and the Caddyfile snippet is included
- Check that DNS for
dmswitch.mitchelltrout.compoints to the server - Verify the API is listening:
curl http://127.0.0.1:3002/api/health
Emails not sending:
- Check
RESEND_API_KEYis set in.env - Check the API logs for email provider errors
- Verify
EMAIL_DAILY_LIMIThasn't been reached (operator gets an alert when this happens)
"rate_limit_deferred" operator alert:
- Daily email quota was hit. Pending alerts are deferred to next UTC midnight.
- Increase
EMAIL_DAILY_LIMITin.envif this happens regularly.
Encryption errors after restore:
- Verify
MASTER_ENCRYPTION_KEYin.envmatches the key used when the data was created - The backup must be restored with the same master key — there is no key migration