house

A family noticeboard fed by WhatsApp. Messages in a family chat land on a private website. Two sources, both without a third party in the path: the WhatsApp Desktop app’s own message store on a Mac (personal account, read-only), or Meta’s official Cloud API (a dedicated house number). Zero runtime dependencies, one process.

See plans/PLAN.md for the design, the block contracts, and setup steps.

Quick start on a Mac with WhatsApp Desktop

npm install                      # dev tooling only (TypeScript, Biome)
node scripts/probe-whatsapp-desktop.mjs   # confirms the store is readable
node bin/house.js chats          # pick the chat(s) to read
cp .env.example .env             # set WHATSAPP_DESKTOP_CHATS=Family and ALLOWED_SENDERS=...
node bin/house.js pull           # read new messages once
node bin/house.js serve          # open http://127.0.0.1:8080

Quick start without WhatsApp

ALLOWED_SENDERS=local node bin/house.js post --from local --name Me "Dinner at 7"
ALLOWED_SENDERS=local node bin/house.js serve
node bin/house.js ingest test/fixtures/whatsapp-mixed.json   # a recorded Cloud API payload

Commands

Command What it does
house serve Read the WhatsApp Desktop store and/or receive Cloud API webhooks, and serve the site
house chats List chats in the WhatsApp Desktop store, to choose WHATSAPP_DESKTOP_CHATS
house pull [--reread] [--rewind N] Read new WhatsApp Desktop messages once; --reread re-maps stored rows, --rewind N moves the cursor back (0 = start)
house post [--from ID] [--name NAME] TEXT Ingest a text message as if it had arrived
house ingest FILE Ingest a recorded webhook payload, a message, or a list of messages
house replay Re-run all handlers over the ledger (after changing a rule)
house messages [--limit N] Print stored messages
house items [--limit N] Print published items
house publish [--force] [--check] Mirror the site into Cloudflare KV now; --check only verifies access

Configuration is read from the environment and ./.env (or $HOUSE_ENV_FILE). Every key is documented in .env.example.

Site

Path Purpose
/ The feed, newest first, live-updating
/api/items?limit=&before= The same as JSON
/media/<file> Downloaded photos, videos, audio, documents
/events Server-sent events, one change event per published item
/healthz Liveness, unauthenticated

Set SITE_USER and SITE_PASSWORD to require HTTP Basic auth on everything except the webhook and /healthz.

Publishing to Cloudflare (free)

The house can mirror the site into a Cloudflare Workers KV namespace, so the family can read it from anywhere. A 60-line Worker of ours serves the page, the item list and the media from KV behind a password. Everything used is on Cloudflare’s free plan and needs no card: KV (100,000 reads and 1,000 writes a day, 1 GB) and Workers. Media over 25 MiB stays on the Mac.

One-time setup on the Mac, in the repo:

npx wrangler login                          # opens a browser
npx wrangler kv namespace create HOUSE      # prints an id: paste it into wrangler.jsonc and .env
npx wrangler deploy                         # prints https://house.<you>.workers.dev
npx wrangler secret put SITE_USER           # the family login; the Worker serves nothing until both are set
npx wrangler secret put SITE_PASSWORD

Then put CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_KV_NAMESPACE_ID and an API token with “Workers KV Storage: Edit” into .env (see .env.example), and:

node bin/house.js publish --check   # verifies the token and namespace
node bin/house.js publish           # writes the page, items.json and media
node bin/house.js serve             # from now on, publishes a minute after each change

What goes to Cloudflare: the published items (author, text, time), the media files they reference, and the page. The ledger and everything the rules do not publish stay on the Mac. Redeploy the Worker only when cloudflare/worker.js changes; the page itself lives in KV and is republished by the house.

Developing

scripts/check.sh    # typecheck + lint + tests; the gate every change must pass
npm test            # tests only

Node 24 runs the TypeScript directly; there is no build step. Biome ships a per-platform binary, so if node_modules was installed on another machine the gate falls back to one under ~/.local/biome/node_modules.

Layout

src/core        message model, ledger, content store, pipeline, config, log
src/http        tiny router and response helpers
src/sources     whatsapp-desktop/ (macOS store reader), whatsapp/ (Cloud API), file (dev)
src/handlers    rules that turn messages into items (publish)
src/site        HTML rendering, routes, basic auth, the static page
src/publish     Cloudflare KV client and the publisher (mirror to the cloud)
cloudflare/     the Worker that serves the mirror; wrangler.jsonc at the root
src/app.ts      composition root
src/cli.ts      commands
test/           node:test suites and recorded webhook fixtures
data/           SQLite database and media (gitignored)