dead-check
v0.1.1
Published
A minimal dead man's switch for your app. If a user goes quiet for N days, someone they trust gets notified.
Maintainers
Readme
dead-check
A minimal dead man's switch for your app.
If a user stops checking in, someone they trust gets notified.
Built for Still Employed — the app where people check in daily to confirm they haven't been replaced by AI. If they go quiet for 2 days, their emergency contact gets an email.
User checks in → streak continues → everyone's fine
User goes quiet → 2 days pass → their person gets an email
Why this exists
The original "Are You OK?" apps (like the Japanese 死んでいませんか) proved one thing: sometimes people just need a quiet, low-stakes way to say "I'm still here."
dead-check is that primitive, extracted into a reusable module. Use it for:
- Employment anxiety — daily check-in to confirm you're still employed (our use case)
- Mental health — gentle "are you okay?" nudge to someone's support person
- Elderly care — family gets notified if grandma doesn't check in
- Habit tracking — accountability partner gets pinged if you skip
- Solo travel — emergency contact notified if you go off-grid too long
- Any app where "going quiet" means something important
How it works
┌─────────────┐ POST /checkin ┌──────────────────┐
│ User │ ──────────────────────▶ │ dead-check API │
│ (daily) │ │ │
└─────────────┘ │ - updates │
│ lastCheckin │
│ - increments │
┌─────────────┐ cron: check daily │ streak │
│ Scheduler │ ──────────────────────▶ │ │
└─────────────┘ │ - finds users │
│ silent for │
│ N days │
┌─────────────┐ sends email │ │
│ Emergency │ ◀────────────────────── │ - notifies │
│ Contact │ │ contact │
└─────────────┘ └──────────────────┘Quick start
npm install dead-checkimport { DeadCheck } from 'dead-check'
const dc = new DeadCheck({
silenceDays: 2, // how many days of silence triggers notification
notifyFn: async (user) => { // bring your own email sender
await sendEmail({
to: user.emergencyEmail,
subject: `Check on ${user.name}`,
body: emailTemplate(user),
})
},
})
// In your check-in endpoint:
await dc.checkin(userId)
// In your daily cron job:
await dc.runCheck() // finds silent users, calls notifyFn for eachThat's it. Two functions.
Full API
new DeadCheck(config)
interface DeadCheckConfig {
silenceDays: number // required. days before notifying
notifyFn: (user: SilentUser) => Promise<void> // required. called once per silent user
db?: DeadCheckAdapter // optional. bring your own DB adapter
onCheckin?: (userId: string) => void // optional. hook after check-in
onNotify?: (user: SilentUser) => void // optional. hook after notification sent
}dc.checkin(userId: string): Promise<CheckinResult>
interface CheckinResult {
userId: string
streak: number // consecutive days checked in
isNew: boolean // first ever check-in
lastSeen: Date
}dc.runCheck(): Promise<CheckResult>
Run this daily via cron. Finds all users whose lastCheckin is older than silenceDays.
interface CheckResult {
checked: number // total users scanned
silent: number // users who triggered notification
notified: number // notifications successfully sent
errors: number // notifications that failed
}dc.getUser(userId: string): Promise<UserStatus>
interface UserStatus {
userId: string
streak: number
lastCheckin: Date | null
isSilent: boolean
daysSinceLast: number
}Database adapters
dead-check ships with a Prisma adapter. Plug in your own schema:
// prisma/schema.prisma — add these fields to your User model:
model User {
id String @id @default(cuid())
// ... your existing fields ...
// dead-check fields:
emergencyEmail String?
lastCheckin DateTime?
streak Int @default(0)
notifiedAt DateTime? // when we last sent the notification
}import { PrismaAdapter } from 'dead-check/adapters/prisma'
import { prisma } from '@/lib/prisma'
const dc = new DeadCheck({
silenceDays: 2,
db: new PrismaAdapter(prisma),
notifyFn: async (user) => { /* ... */ },
})Custom adapter
Implement the DeadCheckAdapter interface to use any database:
interface DeadCheckAdapter {
getUser(userId: string): Promise<DCUser | null>
updateCheckin(userId: string): Promise<DCUser>
getSilentUsers(since: Date): Promise<DCUser[]>
markNotified(userId: string): Promise<void>
}Email templates
dead-check ships with two templates out of the box. Use them, modify them, or write your own.
Default template (warm)
Subject: A quick check — [Name] hasn't checked in
Hey,
You're listed as [Name]'s emergency contact on Still Employed.
They haven't checked in for 2 days. This might mean nothing —
maybe they're on a trip, maybe they just forgot.
But they trusted you enough to put your name down. So we thought
you should know.
— dead-checkMinimal template
Subject: [Name] · 2 days since last check-in
[Name] hasn't checked in since [date].
You're their emergency contact. That's all we know.Usage
import { templates } from 'dead-check/templates'
const dc = new DeadCheck({
silenceDays: 2,
notifyFn: async (user) => {
await resend.emails.send({
to: user.emergencyEmail,
from: '[email protected]',
subject: templates.warm.subject(user),
html: templates.warm.html(user),
})
},
})Next.js example
// app/api/checkin/route.ts
import { dc } from '@/lib/dead-check'
export async function POST(req: Request) {
const session = await getServerSession()
if (!session) return Response.json({ error: 'unauthorized' }, { status: 401 })
const result = await dc.checkin(session.user.id)
return Response.json(result)
}// app/api/cron/dead-check/route.ts
import { dc } from '@/lib/dead-check'
export async function GET(req: Request) {
// Protect with a secret
const auth = req.headers.get('authorization')
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: 'unauthorized' }, { status: 401 })
}
const result = await dc.runCheck()
return Response.json(result)
}// lib/dead-check.ts — singleton
import { DeadCheck, PrismaAdapter } from 'dead-check'
import { prisma } from './prisma'
import { resend } from './resend'
import { templates } from 'dead-check/templates'
export const dc = new DeadCheck({
silenceDays: 2,
db: new PrismaAdapter(prisma),
notifyFn: async (user) => {
await resend.emails.send({
to: user.emergencyEmail!,
from: '[email protected]',
subject: templates.warm.subject(user),
html: templates.warm.html(user),
})
},
})See examples/nextjs for a complete working example.
Vercel Cron setup
// vercel.json
{
"crons": [{
"path": "/api/cron/dead-check",
"schedule": "0 9 * * *"
}]
}# .env
CRON_SECRET=your-secret-hereConfiguration reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| silenceDays | number | — | Required. Days of silence before notifying |
| notifyFn | function | — | Required. Called for each silent user |
| db | adapter | in-memory | Database adapter |
| onCheckin | function | — | Hook after successful check-in |
| onNotify | function | — | Hook after notification sent |
| notifyOnce | boolean | true | Don't re-notify if already notified recently |
| renotifyAfterDays | number | 7 | Days before re-notifying if still silent |
| timezone | string | UTC | Timezone for silence calculation |
Philosophy
dead-check does one thing: notice when someone goes quiet, and tell someone who cares.
It doesn't track location. It doesn't read messages. It doesn't make assumptions about why someone went quiet. It just notices, and nudges.
The logic is under 200 lines. The rest is your app.
Demo
Try dead-check interactively with the built-in demo app:
cd demo
npm install
npm run devOpen http://localhost:3000 — check in, simulate silence, run the check, and see the full notification email output.
Contributing
PRs welcome, especially for:
- New database adapters (Drizzle, Mongoose, Supabase)
- Email provider examples (SendGrid, Postmark, AWS SES)
- New templates
silenceHoursoption for sub-day precision
License
MIT
Built for Still Employed — check in daily, confirm you're still here.
If you stop, someone you trust finds out.
