This site is driven by two keys, like the machine it came from. Reading: j and k scroll, d and u move half a page, gg goes to the top, G to the bottom, H and L switch windows, ? opens the help. Space is the Neovim leader and handles content: Space then h home, r research, p projects, g gear, a about, / tags, or Space then a digit to jump to that window. Home is the tmux prefix and handles windows: Home then c opens a terminal, Home then & closes a window, Home then space goes to the next one. While focus is on the keyboard, h j k l move between keys and Enter opens one.

ResearchR AboutA Gear — G for gear — keyboard, terminal, editor, homelab.G
    ProjectsP Tags — / as in vim: search./
    ×
    Menu

    wa-bot-notif

    WhatsApp notification service — Go + whatsmeow.

    Overview

    A WhatsApp notification service built with Go and whatsmeow. Provides a compact HTTP API for sending messages, checking contacts, and running health checks.

    API

    MethodPathAuthDescription
    POST/sendBearerSend a WhatsApp message
    GET/contactsBearerList synchronized contacts
    GET/messagesBearerRecent message cache (runtime memory)
    GET/healthzAlways returns 200 OK
    GET/readyz200 when WA is connected, 503 otherwise

    POST /send

    Request body:

    { "message": "hello", "userId": "628xxx", "groupId": "[email protected]" }

    Target resolution priority: userIdgroupId → fallback to the GROUP_JID env var.

    Response:

    { "success": true, "sent_to": "<jid>", "timestamp": "<RFC3339>" }

    Configuration

    VariableDefaultRequiredDescription
    AUTH_TOKENBearer token for every authenticated endpoint
    PORT5000HTTP listen port (1–65535)
    GROUP_JIDDefault send target when the request omits userId/groupId
    AUTH_DB_DSNfile:auth.db?_foreign_keys=onSQLite DSN for the WhatsApp session
    LOGS_DB_DSNfile:logs.db?_foreign_keys=onSQLite DSN for the audit log
    LOG_LEVELinfoZerolog level: trace, debug, info, warn, error

    Copy .env.example to .env, then fill in AUTH_TOKEN and GROUP_JID before the first run.

    Running Locally

    Prerequisites

    • Go 1.25+
    • CGO toolchain
    • SQLite dev headers
      • macOS: Xcode Command Line Tools
      • Debian/Ubuntu: build-essential libsqlite3-dev

    Running the service

    GOTOOLCHAIN=auto go run ./cmd/api

    Development-time checks

    GOTOOLCHAIN=auto go test ./...
    GOTOOLCHAIN=auto go vet ./...
    GOTOOLCHAIN=auto gofmt -l .

    Docker

    cp .env.example .env
    # fill in AUTH_TOKEN
    
    docker compose -f deploy/docker-compose.yml up --build -d
    docker compose -f deploy/docker-compose.yml logs -f api

    The SQLite files are persisted via the wa_bot_notif_data Docker volume.

    Shared-network deployment

    In a shared-network deployment, the service is reachable at http://wa-bot-notif-api:5000 through the homelab_integration network:

    docker network create homelab_integration

    Then set INTEGRATION_NETWORK=homelab_integration in .env.

    Project Structure

    .
    ├── cmd/api/          — entry point
    ├── internal/
    │   ├── config/       — env config loading + validation
    │   ├── httpapi/      — HTTP handlers
    │   ├── storage/      — SQLite audit log store
    │   └── wa/           — WhatsApp connection manager
    ├── deploy/           — Docker Compose
    ├── docs/ai/          — AI agent guidance and planning docs
    ├── Dockerfile
    └── go.mod

    Further Reading

    AI Documentation

    • AGENTS.md — agent entrypoint
    • docs/ai/README.md — complete index of AI documentation

    Deployment

    • docs/deploy.md — full deployment runbook (local, Docker, homelab, WireGuard)

    Rewritten from TypeScript/Bun to Go

    The first version was TypeScript on Bun. What runs now is a full rewrite in Go using whatsmeow, and the old files were deleted rather than left sitting beside the new ones.

    Several things were fixed during that rewrite rather than merely translated:

    • Errors swallowed in silence. The _ = ... pattern in httpapi/server.go discarded error values without checking them. All of them are handled now.
    • A token comparison that leaked through timing. Bearer authentication uses crypto/subtle.ConstantTimeCompare rather than ==. An ordinary string comparison stops at the first differing character, so how long it takes to answer tells the caller how much of the guess was right.
    • Missing indexes. logs.timestamp and unauthorized_logs.ip plus its timestamp are now indexed.
    • Logs that grew forever. The log store gained a 30-day retention through its own goroutine.

    Structured logging is zerolog, replacing the standard library’s log.

    id en
    rss gh in