Skip to content

About

Self-hosted dead man's switch with encrypted payload release on missed check-in

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Dead Man's Switch

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

What it does

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.

Running it

pnpm install
pnpm build
pnpm test

Needs PostgreSQL 16 and a master encryption key; the full walkthrough is below.

Status

Working end to end, with an operational runbook and a manual QA checklist. Deployment is documented for Windows Server behind Caddy.


Architecture

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)

Switch States

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).


Windows Server 2022 Installation

Prerequisites

Install these on the server:

  1. Node.js 20 LTS — https://nodejs.org/en/download/
  2. PostgreSQL 16 — https://www.postgresql.org/download/windows/
  3. pnpm — after Node is installed:
    npm install -g pnpm
    
  4. Caddy — https://caddyserver.com/download (place caddy.exe on PATH or install as a Windows service)

1. Create Database

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;

2. Clone and Build

cd C:\srv
git clone <repo-url> deadmansswitch
cd deadmansswitch
pnpm install
pnpm build

3. Configure Environment

# Generate the master encryption key
powershell -File scripts\generate-master-key.ps1

WARNING: Back up MASTER_ENCRYPTION_KEY immediately. 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\.env

Required 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

4. Run Migrations

cd C:\srv\deadmansswitch
pnpm --filter @dms/api db:migrate

5. Start the Server

Foreground (console stays open):

scripts\start.bat

The API starts on 127.0.0.1:3002.

6. Configure Caddy

Paste the contents of Caddyfile.snippet into your existing Caddyfile, then reload:

caddy reload --config C:\srv\caddy\Caddyfile

This serves the React SPA at dmswitch.mitchelltrout.com with automatic HTTPS and proxies /api/* to the Node server.

7. Deploy the Frontend

Copy the built web assets to the Caddy serve directory:

Copy-Item -Recurse -Force packages\web\dist\* C:\srv\deadmansswitch\web\

8. Mint Your First Invite

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.

9. Auto-Start on Boot (Task Scheduler)

  1. Open Task Scheduler → Create Basic Task
  2. Name: DeadMansSwitch
  3. Trigger: When the computer starts
  4. Action: Start a program
    • Program: wscript.exe
    • Arguments: "C:\srv\deadmansswitch\scripts\start-hidden.vbs"
  5. Check Run whether user is logged on or not
  6. Check Run with highest privileges

Upgrade Procedure

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.bat

Backup and Restore

Automated Backups

Run the backup script:

powershell -File C:\srv\deadmansswitch\scripts\backup.ps1

This creates a timestamped directory under C:\srv\deadmansswitch\data\backups\ containing:

  • db.sql — full database dump
  • attachments\ — copy of all encrypted attachments
  • env.gpg — GPG-encrypted .env (if BACKUP_GPG_RECIPIENT is set and gpg is on PATH)

Old backups are pruned after 30 days (configurable via -RetentionDays).

Schedule daily via Task Scheduler:

  1. Create Basic Task → Name: DMS Backup
  2. Trigger: Daily at your preferred time
  3. Action: Start a program
    • Program: powershell.exe
    • Arguments: -File "C:\srv\deadmansswitch\scripts\backup.ps1"

Restore from Backup

See docs/runbook.md for full restore instructions.


Key Management

MASTER_ENCRYPTION_KEY is 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 .env file containing it should be backed up separately (the backup script supports GPG encryption for this)

Admin Operations

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

Manual QA

After deployment, work through docs/qa-checklist.md to verify all critical paths.


Troubleshooting

Server won't start:

  • Check that DATABASE_URL is correct and PostgreSQL is running
  • Verify packages/api/.env exists and has all required variables
  • Check MASTER_ENCRYPTION_KEY is 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.com points to the server
  • Verify the API is listening: curl http://127.0.0.1:3002/api/health

Emails not sending:

  • Check RESEND_API_KEY is set in .env
  • Check the API logs for email provider errors
  • Verify EMAIL_DAILY_LIMIT hasn'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_LIMIT in .env if this happens regularly.

Encryption errors after restore:

  • Verify MASTER_ENCRYPTION_KEY in .env matches the key used when the data was created
  • The backup must be restored with the same master key — there is no key migration

About

Self-hosted dead man's switch with encrypted payload release on missed check-in

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages