Vibecode Dead Man's Snitch
track this build5 phases, 10 steps, beginner friendly0%The entire product is "expect a ping every N, shout when it stops". That is a table, a loop and a webhook. The hosted version earns its fee by being off your box and by the alert routing, not by the code.
You are building a lean indie version of Dead Man's Snitch.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Dead Man's Snitch · indie build
A dead-man's switch for scheduled jobs: each job checks in at a URL, silence past the interval plus grace raises one alert, the next check-in recovers it. The minimal version of heartbeat monitoring, on a box that is not the one running the jobs.
Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | the whole product is a table, a loop and a webhook |
| Hosting | A VPS separate from the jobs | or it is decorative |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **A chat webhook URL (Discord, Slack or Telegram)** · free
- Why: Alerts go to a chat channel you already watch. A webhook URL is the only credential this needs.
- Get it: Discord: Server settings > Integrations > Webhooks > New Webhook, copy the URL. Slack: create an app at api.slack.com/apps, enable Incoming Webhooks, add to a channel, copy the URL. Telegram: create a bot with @BotFather and use the bot token plus your chat id.
- Verify: curl -X POST -H 'Content-Type: application/json' -d '{"content":"test"}' <url> posts a message (Discord form; Slack uses a text field)
- [ ] **The jobs and how often each should check in** · free
- Why: Interval plus grace per snitch.
- Get it: crontab -l and a list.
- [ ] **A small always-on server (VPS)** (optional) · about $5 a month
- Why: This needs one process running all the time with a public address. Separate from the jobs it watches.
- Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first.
- [ ] **Caddy on the server** (optional) · free
- Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work.
- Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config.
- Verify: caddy version prints a version on the server
## Quick start
```sh
mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module
mkdir -p data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts.
- the off-box vantage point
- smart alerts and error-notice parsing
- the integrations (PagerDuty, Slack app, Heroku add-on)
- unlimited team members on one account
If one of those is essential to you, that is the reason to keep paying for Dead Man's Snitch, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Dead Man's Snitch
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a dead-man's-switch monitor for scheduled jobs like Dead Man's Snitch. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22 with node:http and node:sqlite, or Python 3.12 stdlib. No framework, one process, SQLite in WAL mode.
### Data model (create this before Phase 1)
- snitches: id (uuid), name, interval_seconds, grace_seconds, status ('waiting' | 'healthy' | 'missing' | 'paused'), last_checkin_at
- checkins: id, snitch_id, received_at, ok (bool), note
- alerts: id, snitch_id, kind ('missing' | 'recovered'), sent_at, error
UTC epoch milliseconds everywhere.
### Phase 1 · Check-ins
Build: GET, POST and HEAD on /s/:uuid record a check-in and answer 200 with the body OK. A POST body up to 4 kB is stored as the note. /s/:uuid/fail records ok = false. Unknown uuids answer 404.
Done when: a curl records one row and returns OK, a 10 kB body stores 4 kB, and a random uuid returns 404 with no row.
Do not build yet: state, alerts, UI.
### Phase 2 · The switch
Build: a loop every 30 seconds. A snitch is healthy while now is within interval plus grace of its last check-in and becomes missing once past that. A fail check-in flips it missing immediately; the next ok check-in restores healthy. waiting (never checked in) and paused never alert. Each transition writes one alerts row.
Done when: a 1-hour interval with 5-minute grace reads healthy after a check-in and missing at 65 minutes, a fail flips it at once, and a repeated poll in the same state writes nothing.
### Phase 3 · Shouting
Build: a webhook sender (URL in .env) draining alerts rows: one message on missing with how overdue it is, one on recovered with how long it was missing. Three retries with backoff, then record the error.
Done when: one missing produces exactly one message, staying missing produces none, and recovery produces exactly one.
### Phase 4 · Admin
Build: /admin behind basic auth from .env: create, edit, pause and delete snitches; per snitch the URL with a copy button, the crontab line to paste, a status dot and the last 20 check-ins.
Done when: a snitch can be managed end to end in the browser and the page renders with none.
### Phase 5 · Deploy
Build: a /healthz endpoint, a systemd unit, a nightly prune of check-ins older than 30 days, and the README.
Done when: state survives a restart and the README takes a reader from clone to a monitored job.
### Out of scope (and why)
- Smart alerts that parse error output, the PagerDuty and Heroku integrations, team accounts.
- Being off your infrastructure. Put this on a different box than the jobs, or it is decorative.
### README must contain
- The crontab suffix && curl -fsS <url>, and the two-line wrapper that reports failure.
- The same-host warning.
===== AGENTS.md =====
# Agent instructions · Dead Man's Snitch indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS separate from the jobs. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Dead Man's Snitch
A dead-man's switch for scheduled jobs: each job checks in at a URL, silence past the interval plus grace raises one alert, the next check-in recovers it. The minimal version of heartbeat monitoring, on a box that is not the one running the jobs.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Check-ins
GET, POST or HEAD /s/:uuid records a check-in; /fail records a failure; unknown ids 404.
### Steps
1. Create the project and tables
snitches (id, name, interval_seconds, grace_seconds, status, last_checkin_at), checkins (id, snitch_id, received_at, ok, note), alerts.
```sh
mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module
mkdir -p data && cp .env.example .env
```
2. Route /s/:uuid and /s/:uuid/fail; store a POST body up to 4 kB as the note; answer OK
### Done when
- [ ] A curl records one row and prints OK
- [ ] A 10 kB body stores 4 kB
- [ ] A random uuid returns 404
## Phase 2 · The switch
healthy within interval plus grace, missing beyond, fail flips immediately.
### Steps
1. Loop every 30 seconds computing status; waiting and paused never alert
2. Write one alerts row per transition
### Done when
- [ ] 1-hour interval, 5-minute grace reads healthy after a check-in and missing at 65 minutes
- [ ] A fail flips at once
- [ ] Same-state polls write nothing
## Phase 3 · Shouting
One message missing, one recovered.
### Steps
1. Sender for ALERT_FORMAT with how overdue it is
2. Retries with backoff, error recorded on the row
### Done when
- [ ] Exactly one missing message
- [ ] Exactly one recovered message
- [ ] A dead webhook does not stall the loop
## Phase 4 · Admin
Create, pause, delete; copy the URL and the crontab line.
### Steps
1. Basic-auth /admin with CRUD and the URL plus crontab line per snitch
2. Show the last 20 check-ins and a status dot per snitch
### Done when
- [ ] Managed end to end in the browser
- [ ] Renders with none
## Phase 5 · Deploy
HTTPS, a service, retention, README.
### Steps
1. /healthz, systemd, Caddy, nightly prune of check-ins beyond 30 days
2. README with the && curl -fsS suffix, the /fail wrapper and the same-host warning
Files: `README.md`
### Done when
- [ ] State survives a restart
- [ ] The README reaches a monitored job
## Not in this build
- Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts.
## After v1, if you want it
- A weekly summary message of every snitch's health
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Any free port.
PORT=3000
# Required. SQLite file.
DATABASE_PATH=./data/snitches.db
# Required · secret. Chat webhook.
ALERT_WEBHOOK_URL=https://...
# Required. discord, slack or telegram.
ALERT_FORMAT=slack
# Required. Public base URL.
SITE_URL=https://snitch.yourdomain.com
# Required. Any username for the basic-auth admin pages.
ADMIN_USER=admin
# Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password.
ADMIN_PASS=change-me-to-a-long-random-string
You are building a lean indie version of Dead Man's Snitch.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Dead Man's Snitch · indie build
A dead-man's switch for scheduled jobs: each job checks in at a URL, silence past the interval plus grace raises one alert, the next check-in recovers it. The minimal version of heartbeat monitoring, on a box that is not the one running the jobs.
Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | the whole product is a table, a loop and a webhook |
| Hosting | A VPS separate from the jobs | or it is decorative |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **A chat webhook URL (Discord, Slack or Telegram)** · free
- Why: Alerts go to a chat channel you already watch. A webhook URL is the only credential this needs.
- Get it: Discord: Server settings > Integrations > Webhooks > New Webhook, copy the URL. Slack: create an app at api.slack.com/apps, enable Incoming Webhooks, add to a channel, copy the URL. Telegram: create a bot with @BotFather and use the bot token plus your chat id.
- Verify: curl -X POST -H 'Content-Type: application/json' -d '{"content":"test"}' <url> posts a message (Discord form; Slack uses a text field)
- [ ] **The jobs and how often each should check in** · free
- Why: Interval plus grace per snitch.
- Get it: crontab -l and a list.
- [ ] **A small always-on server (VPS)** (optional) · about $5 a month
- Why: This needs one process running all the time with a public address. Separate from the jobs it watches.
- Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first.
- [ ] **Caddy on the server** (optional) · free
- Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work.
- Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config.
- Verify: caddy version prints a version on the server
## Quick start
```sh
mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module
mkdir -p data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts.
- the off-box vantage point
- smart alerts and error-notice parsing
- the integrations (PagerDuty, Slack app, Heroku add-on)
- unlimited team members on one account
If one of those is essential to you, that is the reason to keep paying for Dead Man's Snitch, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Dead Man's Snitch
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a dead-man's-switch monitor for scheduled jobs like Dead Man's Snitch. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22 with node:http and node:sqlite, or Python 3.12 stdlib. No framework, one process, SQLite in WAL mode.
### Data model (create this before Phase 1)
- snitches: id (uuid), name, interval_seconds, grace_seconds, status ('waiting' | 'healthy' | 'missing' | 'paused'), last_checkin_at
- checkins: id, snitch_id, received_at, ok (bool), note
- alerts: id, snitch_id, kind ('missing' | 'recovered'), sent_at, error
UTC epoch milliseconds everywhere.
### Phase 1 · Check-ins
Build: GET, POST and HEAD on /s/:uuid record a check-in and answer 200 with the body OK. A POST body up to 4 kB is stored as the note. /s/:uuid/fail records ok = false. Unknown uuids answer 404.
Done when: a curl records one row and returns OK, a 10 kB body stores 4 kB, and a random uuid returns 404 with no row.
Do not build yet: state, alerts, UI.
### Phase 2 · The switch
Build: a loop every 30 seconds. A snitch is healthy while now is within interval plus grace of its last check-in and becomes missing once past that. A fail check-in flips it missing immediately; the next ok check-in restores healthy. waiting (never checked in) and paused never alert. Each transition writes one alerts row.
Done when: a 1-hour interval with 5-minute grace reads healthy after a check-in and missing at 65 minutes, a fail flips it at once, and a repeated poll in the same state writes nothing.
### Phase 3 · Shouting
Build: a webhook sender (URL in .env) draining alerts rows: one message on missing with how overdue it is, one on recovered with how long it was missing. Three retries with backoff, then record the error.
Done when: one missing produces exactly one message, staying missing produces none, and recovery produces exactly one.
### Phase 4 · Admin
Build: /admin behind basic auth from .env: create, edit, pause and delete snitches; per snitch the URL with a copy button, the crontab line to paste, a status dot and the last 20 check-ins.
Done when: a snitch can be managed end to end in the browser and the page renders with none.
### Phase 5 · Deploy
Build: a /healthz endpoint, a systemd unit, a nightly prune of check-ins older than 30 days, and the README.
Done when: state survives a restart and the README takes a reader from clone to a monitored job.
### Out of scope (and why)
- Smart alerts that parse error output, the PagerDuty and Heroku integrations, team accounts.
- Being off your infrastructure. Put this on a different box than the jobs, or it is decorative.
### README must contain
- The crontab suffix && curl -fsS <url>, and the two-line wrapper that reports failure.
- The same-host warning.
===== AGENTS.md =====
# Agent instructions · Dead Man's Snitch indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS separate from the jobs. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Dead Man's Snitch
A dead-man's switch for scheduled jobs: each job checks in at a URL, silence past the interval plus grace raises one alert, the next check-in recovers it. The minimal version of heartbeat monitoring, on a box that is not the one running the jobs.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Check-ins
GET, POST or HEAD /s/:uuid records a check-in; /fail records a failure; unknown ids 404.
### Steps
1. Create the project and tables
snitches (id, name, interval_seconds, grace_seconds, status, last_checkin_at), checkins (id, snitch_id, received_at, ok, note), alerts.
```sh
mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module
mkdir -p data && cp .env.example .env
```
2. Route /s/:uuid and /s/:uuid/fail; store a POST body up to 4 kB as the note; answer OK
### Done when
- [ ] A curl records one row and prints OK
- [ ] A 10 kB body stores 4 kB
- [ ] A random uuid returns 404
## Phase 2 · The switch
healthy within interval plus grace, missing beyond, fail flips immediately.
### Steps
1. Loop every 30 seconds computing status; waiting and paused never alert
2. Write one alerts row per transition
### Done when
- [ ] 1-hour interval, 5-minute grace reads healthy after a check-in and missing at 65 minutes
- [ ] A fail flips at once
- [ ] Same-state polls write nothing
## Phase 3 · Shouting
One message missing, one recovered.
### Steps
1. Sender for ALERT_FORMAT with how overdue it is
2. Retries with backoff, error recorded on the row
### Done when
- [ ] Exactly one missing message
- [ ] Exactly one recovered message
- [ ] A dead webhook does not stall the loop
## Phase 4 · Admin
Create, pause, delete; copy the URL and the crontab line.
### Steps
1. Basic-auth /admin with CRUD and the URL plus crontab line per snitch
2. Show the last 20 check-ins and a status dot per snitch
### Done when
- [ ] Managed end to end in the browser
- [ ] Renders with none
## Phase 5 · Deploy
HTTPS, a service, retention, README.
### Steps
1. /healthz, systemd, Caddy, nightly prune of check-ins beyond 30 days
2. README with the && curl -fsS suffix, the /fail wrapper and the same-host warning
Files: `README.md`
### Done when
- [ ] State survives a restart
- [ ] The README reaches a monitored job
## Not in this build
- Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts.
## After v1, if you want it
- A weekly summary message of every snitch's health
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Any free port.
PORT=3000
# Required. SQLite file.
DATABASE_PATH=./data/snitches.db
# Required · secret. Chat webhook.
ALERT_WEBHOOK_URL=https://...
# Required. discord, slack or telegram.
ALERT_FORMAT=slack
# Required. Public base URL.
SITE_URL=https://snitch.yourdomain.com
# Required. Any username for the basic-auth admin pages.
ADMIN_USER=admin
# Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password.
ADMIN_PASS=change-me-to-a-long-random-string
You are building a production product version of Dead Man's Snitch.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# Dead Man's Snitch · product brief
## Problem
The entire product is "expect a ping every N, shout when it stops". That is a table, a loop and a webhook. The hosted version earns its fee by being off your box and by the alert routing, not by the code.
## Product outcome
The smallest reliable heartbeat service, run from a box that is not the jobs' and itself watched.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- an always-on host separate from the jobs
- a chat webhook
## Explicit non-goals for v1
- Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts.
- the off-box vantage point
- smart alerts and error-notice parsing
- the integrations (PagerDuty, Slack app, Heroku add-on)
- unlimited team members on one account
## Success criteria
- Different box than the jobs
- One restore drill performed
===== BRIEF.md =====
# Build brief · Dead Man's Snitch
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a dead-man's-switch monitor for scheduled jobs like Dead Man's Snitch. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22 with node:http and node:sqlite, or Python 3.12 stdlib. No framework, one process, SQLite in WAL mode.
### Data model (create this before Phase 1)
- snitches: id (uuid), name, interval_seconds, grace_seconds, status ('waiting' | 'healthy' | 'missing' | 'paused'), last_checkin_at
- checkins: id, snitch_id, received_at, ok (bool), note
- alerts: id, snitch_id, kind ('missing' | 'recovered'), sent_at, error
UTC epoch milliseconds everywhere.
### Phase 1 · Check-ins
Build: GET, POST and HEAD on /s/:uuid record a check-in and answer 200 with the body OK. A POST body up to 4 kB is stored as the note. /s/:uuid/fail records ok = false. Unknown uuids answer 404.
Done when: a curl records one row and returns OK, a 10 kB body stores 4 kB, and a random uuid returns 404 with no row.
Do not build yet: state, alerts, UI.
### Phase 2 · The switch
Build: a loop every 30 seconds. A snitch is healthy while now is within interval plus grace of its last check-in and becomes missing once past that. A fail check-in flips it missing immediately; the next ok check-in restores healthy. waiting (never checked in) and paused never alert. Each transition writes one alerts row.
Done when: a 1-hour interval with 5-minute grace reads healthy after a check-in and missing at 65 minutes, a fail flips it at once, and a repeated poll in the same state writes nothing.
### Phase 3 · Shouting
Build: a webhook sender (URL in .env) draining alerts rows: one message on missing with how overdue it is, one on recovered with how long it was missing. Three retries with backoff, then record the error.
Done when: one missing produces exactly one message, staying missing produces none, and recovery produces exactly one.
### Phase 4 · Admin
Build: /admin behind basic auth from .env: create, edit, pause and delete snitches; per snitch the URL with a copy button, the crontab line to paste, a status dot and the last 20 check-ins.
Done when: a snitch can be managed end to end in the browser and the page renders with none.
### Phase 5 · Deploy
Build: a /healthz endpoint, a systemd unit, a nightly prune of check-ins older than 30 days, and the README.
Done when: state survives a restart and the README takes a reader from clone to a monitored job.
### Out of scope (and why)
- Smart alerts that parse error output, the PagerDuty and Heroku integrations, team accounts.
- Being off your infrastructure. Put this on a different box than the jobs, or it is decorative.
### README must contain
- The crontab suffix && curl -fsS <url>, and the two-line wrapper that reports failure.
- The same-host warning.
===== ARCHITECTURE.md =====
# Architecture · Dead Man's Snitch
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | the whole product is a table, a loop and a webhook |
| Hosting | A VPS separate from the jobs | or it is decorative |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Ingest | /s routes | Any listener writing the rows |
| Switch | the loop | Pure function |
| Notifier | messages and retries | One function per format |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `PORT` · required · Any free port.
- `DATABASE_PATH` · required · SQLite file.
- `ALERT_WEBHOOK_URL` · required, secret · Chat webhook.
- `ALERT_FORMAT` · required · discord, slack or telegram.
- `SITE_URL` · required · Public base URL.
- `ADMIN_USER` · required · Any username for the basic-auth admin pages.
- `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · Dead Man's Snitch product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS separate from the jobs.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
===== MILESTONES.md =====
# Delivery milestones · Dead Man's Snitch
Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Check-ins
GET, POST or HEAD /s/:uuid records a check-in; /fail records a failure; unknown ids 404.
### Steps
1. Create the project and tables
snitches (id, name, interval_seconds, grace_seconds, status, last_checkin_at), checkins (id, snitch_id, received_at, ok, note), alerts.
```sh
mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module
mkdir -p data && cp .env.example .env
```
2. Route /s/:uuid and /s/:uuid/fail; store a POST body up to 4 kB as the note; answer OK
### Done when
- [ ] A curl records one row and prints OK
- [ ] A 10 kB body stores 4 kB
- [ ] A random uuid returns 404
## M2 · The switch
healthy within interval plus grace, missing beyond, fail flips immediately.
### Steps
1. Loop every 30 seconds computing status; waiting and paused never alert
2. Write one alerts row per transition
### Done when
- [ ] 1-hour interval, 5-minute grace reads healthy after a check-in and missing at 65 minutes
- [ ] A fail flips at once
- [ ] Same-state polls write nothing
## M3 · Shouting
One message missing, one recovered.
### Steps
1. Sender for ALERT_FORMAT with how overdue it is
2. Retries with backoff, error recorded on the row
### Done when
- [ ] Exactly one missing message
- [ ] Exactly one recovered message
- [ ] A dead webhook does not stall the loop
## M4 · Admin
Create, pause, delete; copy the URL and the crontab line.
### Steps
1. Basic-auth /admin with CRUD and the URL plus crontab line per snitch
2. Show the last 20 check-ins and a status dot per snitch
### Done when
- [ ] Managed end to end in the browser
- [ ] Renders with none
## M5 · Deploy
HTTPS, a service, retention, README.
### Steps
1. /healthz, systemd, Caddy, nightly prune of check-ins beyond 30 days
2. README with the && curl -fsS suffix, the /fail wrapper and the same-host warning
Files: `README.md`
### Done when
- [ ] State survives a restart
- [ ] The README reaches a monitored job
## M6 · Operate it like a product (production only)
Only for the product-builder path: know when the switch is down, never lose the database, and keep the server patched.
### Steps
1. Add a /healthz endpoint and an external uptime check against it
Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices.
2. Write structured request logs and rotate them
One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight.
3. Back the SQLite file up off the machine nightly and test a restore
SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it.
```sh
sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'"
rclone copy /tmp/app-$(date +%F).db remote:backups/
```
4. Lock the box down
Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure.
### Done when
- [ ] Stopping the service triggers an uptime alert within a few minutes
- [ ] A restore from last night's backup contains yesterday's data
- [ ] A port scan from another machine shows only 22, 80 and 443
===== OPERATIONS.md =====
# Operations · Dead Man's Snitch
## Backup
SQLite .backup nightly.
## Restore
Copy back.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
External check on /healthz.
## Incident checklist
Down switch means unwatched jobs; restore and review.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] Different box than the jobs
- [ ] One restore drill performed
## Launch constraint
Do not market omitted Dead Man's Snitch capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Any free port.
PORT=3000
# Required. SQLite file.
DATABASE_PATH=./data/snitches.db
# Required · secret. Chat webhook.
ALERT_WEBHOOK_URL=https://...
# Required. discord, slack or telegram.
ALERT_FORMAT=slack
# Required. Public base URL.
SITE_URL=https://snitch.yourdomain.com
# Required. Any username for the basic-auth admin pages.
ADMIN_USER=admin
# Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password.
ADMIN_PASS=change-me-to-a-long-random-string
# Dead Man's Snitch · indie build
A dead-man's switch for scheduled jobs: each job checks in at a URL, silence past the interval plus grace raises one alert, the next check-in recovers it. The minimal version of heartbeat monitoring, on a box that is not the one running the jobs.
Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | the whole product is a table, a loop and a webhook |
| Hosting | A VPS separate from the jobs | or it is decorative |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **A chat webhook URL (Discord, Slack or Telegram)** · free
- Why: Alerts go to a chat channel you already watch. A webhook URL is the only credential this needs.
- Get it: Discord: Server settings > Integrations > Webhooks > New Webhook, copy the URL. Slack: create an app at api.slack.com/apps, enable Incoming Webhooks, add to a channel, copy the URL. Telegram: create a bot with @BotFather and use the bot token plus your chat id.
- Verify: curl -X POST -H 'Content-Type: application/json' -d '{"content":"test"}' <url> posts a message (Discord form; Slack uses a text field)
- [ ] **The jobs and how often each should check in** · free
- Why: Interval plus grace per snitch.
- Get it: crontab -l and a list.
- [ ] **A small always-on server (VPS)** (optional) · about $5 a month
- Why: This needs one process running all the time with a public address. Separate from the jobs it watches.
- Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first.
- [ ] **Caddy on the server** (optional) · free
- Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work.
- Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config.
- Verify: caddy version prints a version on the server
## Quick start
```sh
mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module
mkdir -p data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts.
- the off-box vantage point
- smart alerts and error-notice parsing
- the integrations (PagerDuty, Slack app, Heroku add-on)
- unlimited team members on one account
If one of those is essential to you, that is the reason to keep paying for Dead Man's Snitch, and the README should say so rather than pretend.# Build brief · Dead Man's Snitch
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a dead-man's-switch monitor for scheduled jobs like Dead Man's Snitch. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22 with node:http and node:sqlite, or Python 3.12 stdlib. No framework, one process, SQLite in WAL mode.
### Data model (create this before Phase 1)
- snitches: id (uuid), name, interval_seconds, grace_seconds, status ('waiting' | 'healthy' | 'missing' | 'paused'), last_checkin_at
- checkins: id, snitch_id, received_at, ok (bool), note
- alerts: id, snitch_id, kind ('missing' | 'recovered'), sent_at, error
UTC epoch milliseconds everywhere.
### Phase 1 · Check-ins
Build: GET, POST and HEAD on /s/:uuid record a check-in and answer 200 with the body OK. A POST body up to 4 kB is stored as the note. /s/:uuid/fail records ok = false. Unknown uuids answer 404.
Done when: a curl records one row and returns OK, a 10 kB body stores 4 kB, and a random uuid returns 404 with no row.
Do not build yet: state, alerts, UI.
### Phase 2 · The switch
Build: a loop every 30 seconds. A snitch is healthy while now is within interval plus grace of its last check-in and becomes missing once past that. A fail check-in flips it missing immediately; the next ok check-in restores healthy. waiting (never checked in) and paused never alert. Each transition writes one alerts row.
Done when: a 1-hour interval with 5-minute grace reads healthy after a check-in and missing at 65 minutes, a fail flips it at once, and a repeated poll in the same state writes nothing.
### Phase 3 · Shouting
Build: a webhook sender (URL in .env) draining alerts rows: one message on missing with how overdue it is, one on recovered with how long it was missing. Three retries with backoff, then record the error.
Done when: one missing produces exactly one message, staying missing produces none, and recovery produces exactly one.
### Phase 4 · Admin
Build: /admin behind basic auth from .env: create, edit, pause and delete snitches; per snitch the URL with a copy button, the crontab line to paste, a status dot and the last 20 check-ins.
Done when: a snitch can be managed end to end in the browser and the page renders with none.
### Phase 5 · Deploy
Build: a /healthz endpoint, a systemd unit, a nightly prune of check-ins older than 30 days, and the README.
Done when: state survives a restart and the README takes a reader from clone to a monitored job.
### Out of scope (and why)
- Smart alerts that parse error output, the PagerDuty and Heroku integrations, team accounts.
- Being off your infrastructure. Put this on a different box than the jobs, or it is decorative.
### README must contain
- The crontab suffix && curl -fsS <url>, and the two-line wrapper that reports failure.
- The same-host warning.# Agent instructions · Dead Man's Snitch indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS separate from the jobs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · Dead Man's Snitch A dead-man's switch for scheduled jobs: each job checks in at a URL, silence past the interval plus grace raises one alert, the next check-in recovers it. The minimal version of heartbeat monitoring, on a box that is not the one running the jobs. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Check-ins GET, POST or HEAD /s/:uuid records a check-in; /fail records a failure; unknown ids 404. ### Steps 1. Create the project and tables snitches (id, name, interval_seconds, grace_seconds, status, last_checkin_at), checkins (id, snitch_id, received_at, ok, note), alerts. ```sh mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Route /s/:uuid and /s/:uuid/fail; store a POST body up to 4 kB as the note; answer OK ### Done when - [ ] A curl records one row and prints OK - [ ] A 10 kB body stores 4 kB - [ ] A random uuid returns 404 ## Phase 2 · The switch healthy within interval plus grace, missing beyond, fail flips immediately. ### Steps 1. Loop every 30 seconds computing status; waiting and paused never alert 2. Write one alerts row per transition ### Done when - [ ] 1-hour interval, 5-minute grace reads healthy after a check-in and missing at 65 minutes - [ ] A fail flips at once - [ ] Same-state polls write nothing ## Phase 3 · Shouting One message missing, one recovered. ### Steps 1. Sender for ALERT_FORMAT with how overdue it is 2. Retries with backoff, error recorded on the row ### Done when - [ ] Exactly one missing message - [ ] Exactly one recovered message - [ ] A dead webhook does not stall the loop ## Phase 4 · Admin Create, pause, delete; copy the URL and the crontab line. ### Steps 1. Basic-auth /admin with CRUD and the URL plus crontab line per snitch 2. Show the last 20 check-ins and a status dot per snitch ### Done when - [ ] Managed end to end in the browser - [ ] Renders with none ## Phase 5 · Deploy HTTPS, a service, retention, README. ### Steps 1. /healthz, systemd, Caddy, nightly prune of check-ins beyond 30 days 2. README with the && curl -fsS suffix, the /fail wrapper and the same-host warning Files: `README.md` ### Done when - [ ] State survives a restart - [ ] The README reaches a monitored job ## Not in this build - Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts. ## After v1, if you want it - A weekly summary message of every snitch's health
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/snitches.db # Required · secret. Chat webhook. ALERT_WEBHOOK_URL=https://... # Required. discord, slack or telegram. ALERT_FORMAT=slack # Required. Public base URL. SITE_URL=https://snitch.yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
# Dead Man's Snitch · product brief ## Problem The entire product is "expect a ping every N, shout when it stops". That is a table, a loop and a webhook. The hosted version earns its fee by being off your box and by the alert routing, not by the code. ## Product outcome The smallest reliable heartbeat service, run from a box that is not the jobs' and itself watched. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - an always-on host separate from the jobs - a chat webhook ## Explicit non-goals for v1 - Smart alerts that parse output, PagerDuty and Heroku integrations, team accounts. - the off-box vantage point - smart alerts and error-notice parsing - the integrations (PagerDuty, Slack app, Heroku add-on) - unlimited team members on one account ## Success criteria - Different box than the jobs - One restore drill performed
# Build brief · Dead Man's Snitch
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a dead-man's-switch monitor for scheduled jobs like Dead Man's Snitch. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22 with node:http and node:sqlite, or Python 3.12 stdlib. No framework, one process, SQLite in WAL mode.
### Data model (create this before Phase 1)
- snitches: id (uuid), name, interval_seconds, grace_seconds, status ('waiting' | 'healthy' | 'missing' | 'paused'), last_checkin_at
- checkins: id, snitch_id, received_at, ok (bool), note
- alerts: id, snitch_id, kind ('missing' | 'recovered'), sent_at, error
UTC epoch milliseconds everywhere.
### Phase 1 · Check-ins
Build: GET, POST and HEAD on /s/:uuid record a check-in and answer 200 with the body OK. A POST body up to 4 kB is stored as the note. /s/:uuid/fail records ok = false. Unknown uuids answer 404.
Done when: a curl records one row and returns OK, a 10 kB body stores 4 kB, and a random uuid returns 404 with no row.
Do not build yet: state, alerts, UI.
### Phase 2 · The switch
Build: a loop every 30 seconds. A snitch is healthy while now is within interval plus grace of its last check-in and becomes missing once past that. A fail check-in flips it missing immediately; the next ok check-in restores healthy. waiting (never checked in) and paused never alert. Each transition writes one alerts row.
Done when: a 1-hour interval with 5-minute grace reads healthy after a check-in and missing at 65 minutes, a fail flips it at once, and a repeated poll in the same state writes nothing.
### Phase 3 · Shouting
Build: a webhook sender (URL in .env) draining alerts rows: one message on missing with how overdue it is, one on recovered with how long it was missing. Three retries with backoff, then record the error.
Done when: one missing produces exactly one message, staying missing produces none, and recovery produces exactly one.
### Phase 4 · Admin
Build: /admin behind basic auth from .env: create, edit, pause and delete snitches; per snitch the URL with a copy button, the crontab line to paste, a status dot and the last 20 check-ins.
Done when: a snitch can be managed end to end in the browser and the page renders with none.
### Phase 5 · Deploy
Build: a /healthz endpoint, a systemd unit, a nightly prune of check-ins older than 30 days, and the README.
Done when: state survives a restart and the README takes a reader from clone to a monitored job.
### Out of scope (and why)
- Smart alerts that parse error output, the PagerDuty and Heroku integrations, team accounts.
- Being off your infrastructure. Put this on a different box than the jobs, or it is decorative.
### README must contain
- The crontab suffix && curl -fsS <url>, and the two-line wrapper that reports failure.
- The same-host warning.# Architecture · Dead Man's Snitch ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | the whole product is a table, a loop and a webhook | | Hosting | A VPS separate from the jobs | or it is decorative | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Ingest | /s routes | Any listener writing the rows | | Switch | the loop | Pure function | | Notifier | messages and retries | One function per format | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `ALERT_WEBHOOK_URL` · required, secret · Chat webhook. - `ALERT_FORMAT` · required · discord, slack or telegram. - `SITE_URL` · required · Public base URL. - `ADMIN_USER` · required · Any username for the basic-auth admin pages. - `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Dead Man's Snitch product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, A VPS separate from the jobs. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · Dead Man's Snitch Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Check-ins GET, POST or HEAD /s/:uuid records a check-in; /fail records a failure; unknown ids 404. ### Steps 1. Create the project and tables snitches (id, name, interval_seconds, grace_seconds, status, last_checkin_at), checkins (id, snitch_id, received_at, ok, note), alerts. ```sh mkdir snitch && cd snitch && git init && npm init -y && npm pkg set type=module mkdir -p data && cp .env.example .env ``` 2. Route /s/:uuid and /s/:uuid/fail; store a POST body up to 4 kB as the note; answer OK ### Done when - [ ] A curl records one row and prints OK - [ ] A 10 kB body stores 4 kB - [ ] A random uuid returns 404 ## M2 · The switch healthy within interval plus grace, missing beyond, fail flips immediately. ### Steps 1. Loop every 30 seconds computing status; waiting and paused never alert 2. Write one alerts row per transition ### Done when - [ ] 1-hour interval, 5-minute grace reads healthy after a check-in and missing at 65 minutes - [ ] A fail flips at once - [ ] Same-state polls write nothing ## M3 · Shouting One message missing, one recovered. ### Steps 1. Sender for ALERT_FORMAT with how overdue it is 2. Retries with backoff, error recorded on the row ### Done when - [ ] Exactly one missing message - [ ] Exactly one recovered message - [ ] A dead webhook does not stall the loop ## M4 · Admin Create, pause, delete; copy the URL and the crontab line. ### Steps 1. Basic-auth /admin with CRUD and the URL plus crontab line per snitch 2. Show the last 20 check-ins and a status dot per snitch ### Done when - [ ] Managed end to end in the browser - [ ] Renders with none ## M5 · Deploy HTTPS, a service, retention, README. ### Steps 1. /healthz, systemd, Caddy, nightly prune of check-ins beyond 30 days 2. README with the && curl -fsS suffix, the /fail wrapper and the same-host warning Files: `README.md` ### Done when - [ ] State survives a restart - [ ] The README reaches a monitored job ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the switch is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · Dead Man's Snitch ## Backup SQLite .backup nightly. ## Restore Copy back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring External check on /healthz. ## Incident checklist Down switch means unwatched jobs; restore and review. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Different box than the jobs - [ ] One restore drill performed ## Launch constraint Do not market omitted Dead Man's Snitch capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/snitches.db # Required · secret. Chat webhook. ALERT_WEBHOOK_URL=https://... # Required. discord, slack or telegram. ALERT_FORMAT=slack # Required. Public base URL. SITE_URL=https://snitch.yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because a monitor that lives on the same host as the jobs is silent exactly when the host is, and because the Heroku and PagerDuty plumbing is already done.
xthe off-box vantage point
xsmart alerts and error-notice parsing
xthe integrations (PagerDuty, Slack app, Heroku add-on)
xunlimited team members on one account
Dead Man's Snitch pricing
private eye$19/mo · monthly flat · $228/yr
free tierThe free Lone Snitch plan monitors exactly one job.
verified 2026-09-04 · source ↗
Is Dead Man's Snitch free?
The free Lone Snitch plan monitors exactly one job. Paid is Private Eye at $19/mo (checked 2026-09-04).
Vibecode Dead Man's Snitch
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Dead Man's Snitch replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Dead Man's Snitch cost?
Dead Man's Snitch costs about $19/month (Private Eye, checked 2026-09-04), which is $228 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Dead Man's Snitch?
Honestly: the off-box vantage point; smart alerts and error-notice parsing; the integrations (PagerDuty, Slack app, Heroku add-on); unlimited team members on one account. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Dead Man's Snitch?
Yes: healthchecks (open-source equivalent with the same ping-URL model). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.