sendword 0.9.0

Simple HTTP webhook to command runner sidecar. Frontend for managing hooks, JSON state for config portability, SQLite for execution history and logs.
Documentation

sendword

HTTP webhook receiver that runs commands. Define hooks in TOML, trigger them with HTTP requests, see results in a web dashboard.

Documentation | Install

What it does

sendword sits next to your application as a sidecar. It listens for incoming webhooks and executes shell commands, scripts, or HTTP calls in response. Configuration lives in a single TOML file. Execution history and logs are stored in SQLite.

GitHub/CI/Monitoring ──HTTP POST──▶ sendword ──▶ shell command
                                                  script
                                                  JavaScript
                                                  Python
                                                  HTTP request

Features

  • Webhook authentication --- Bearer tokens, HMAC-SHA256 signature verification, or open access. Constant-time comparison prevents timing attacks.
  • Payload validation --- JSON schema per hook. Malformed payloads are rejected before your command runs.
  • Trigger rules --- Filter by payload fields, restrict to time windows, enforce cooldowns, and apply rate limits before execution.
  • Script runtime executors --- Shell commands with payload interpolation, executable scripts, JavaScript, Python, or HTTP forwarding. Per-hook timeouts and working directories.
  • Retries with backoff --- None, linear, or exponential. Configurable per hook or globally, with max delay caps.
  • Execution barriers --- Mutex or queue-based concurrency control. Approval workflows gate hooks behind human review with optional timeouts.
  • Secret masking --- Redact env var values and regex pattern matches from dashboard log views.
  • Backup and restore --- Snapshot database and config to S3-compatible storage on a cron schedule. Restore with one command. Retention policies handle cleanup.
  • Web dashboard --- View hooks, executions, trigger attempts, and stream logs in real time via SSE.
  • Config portability --- Export/import configuration as JSON. Environment variable overrides for every setting.

Quick start

Install

Install from crates.io if you already have a Rust toolchain:

cargo install sendword

Prebuilt release artifacts are also published for Linux x86_64 and Windows x86_64. The installers put sendword in $HOME/.cargo/bin by default.

Linux x86_64:

curl --proto '=https' --tlsv1.2 -LsSf https://releases.sendword.online/latest/sendword-installer.sh | sh

Windows PowerShell:

powershell -ExecutionPolicy Bypass -c "irm https://releases.sendword.online/latest/sendword-installer.ps1 | iex"

The installers default to latest. To pin a release, set SENDWORD_VERSION to a version tag:

curl --proto '=https' --tlsv1.2 -LsSf https://releases.sendword.online/latest/sendword-installer.sh | SENDWORD_VERSION=v0.8.7 sh

Use Docker when you want sendword and its script runtimes packaged together:

mkdir -p data
docker run --rm -p 8080:8080 -v "$PWD/data:/data" ghcr.io/wavefunk/sendword:latest

Manual archives and checksums are available under https://releases.sendword.online/latest/. Replace latest with a version tag such as v0.8.7 to download a specific release.

Build from source

Source builds require Rust nightly. The project pins nightly-2026-01-05 via rust-toolchain.toml.

With Nix and direnv, dependencies are managed automatically:

direnv allow
# Build
cargo build --release

# Run (creates data/ directory and SQLite database on first start)
./target/release/sendword serve

Or during development:

cargo run

sendword starts on 127.0.0.1:8080 by default. Open it in a browser to access the dashboard.

Create a user

The dashboard requires authentication. Create your first user:

sendword user create --email you@example.com

You'll be prompted for a password.

Configuration

sendword loads config from sendword.toml, then sendword.json, then environment variables (in that priority order). Environment variables use the SENDWORD_<SECTION>__<KEY> format.

Minimal example

[server]
bind = "127.0.0.1"
port = 8080

[[hooks]]
name = "Deploy App"
slug = "deploy-app"

[hooks.executor]
type = "shell"
command = "cd /opt/app && git pull && make deploy"

This creates a hook at POST /hook/deploy-app that runs the deploy command.

Full example

[server]
bind = "127.0.0.1"
port = 9090

[database]
path = "data/sendword.db"

[logs]
dir = "data/logs"

[auth]
session_lifetime = "24h"
secure_cookie = false

[auth.smtp]
host = "smtp.example.com"
port = 587
username = "sendword@example.com"
password = "your-smtp-password"
from = "sendword@example.com"
starttls = true

[scripts]
dir = "data/scripts"

[defaults]
timeout = "30s"

[defaults.rate_limit]
max_per_minute = 60

[defaults.retries]
count = 0
backoff = "exponential"
initial_delay = "1s"
max_delay = "60s"

[masking]
env_vars = ["DATABASE_URL", "API_KEY", "AWS_SECRET_ACCESS_KEY"]
patterns = ["Bearer [A-Za-z0-9._~+/=-]+", "ghp_[A-Za-z0-9]{36}"]

[[hooks]]
name = "Deploy App"
slug = "deploy-app"
description = "Triggers app deployment"
enabled = true
cwd = "/opt/app"
timeout = "120s"

[hooks.auth]
mode = "bearer"
token = "secret-deploy-token"

[hooks.executor]
type = "shell"
command = "echo 'deploying $APP_ENV'"

[hooks.env]
APP_ENV = "production"

[hooks.retries]
count = 2
backoff = "exponential"
initial_delay = "2s"
max_delay = "30s"

[hooks.rate_limit]
max_per_minute = 5

[hooks.trigger_rules]
cooldown = "30s"
payload_filters = [{ field = "action", operator = "equals", value = "deploy" }]

[hooks.trigger_rules.rate_limit]
max_requests = 10
window = "1h"

[[hooks.trigger_rules.time_windows]]
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
start_time = "09:00"
end_time = "17:00"

[hooks.concurrency]
mode = "mutex"

[hooks.approval]
required = true
timeout = "30m"

[hooks.notification]
url = "https://hooks.slack.com/services/T00/B00/xxx"
on = ["failure", "timeout"]
body = '{"text": "Hook {{hook_name}} {{outcome}}"}'

[backup]
endpoint = "https://s3.amazonaws.com"
bucket = "sendword-backups"
access_key = "AKIA..."
secret_key = "..."
region = "us-east-1"
prefix = "prod/"
schedule = "0 0 3 * * *"

[backup.retention]
max_count = 30
max_age = "90d"

Masking is applied when logs are read for the dashboard; raw log files are left unchanged. Env-var value masks stream live with a small retained suffix. Regex masks are applied when the full log content can be evaluated, so running executions with regex masks withhold live log output until the execution reaches a terminal state.

Executor types

Shell --- runs a command in a shell process:

[hooks.executor]
type = "shell"
command = "deploy.sh --env production"

Script --- runs an executable script directly. The file needs executable permissions and a shebang:

[hooks.executor]
type = "script"
path = "data/scripts/deploy.sh"

JavaScript --- runs a script with node:

[hooks.executor]
type = "javascript"
path = "data/scripts/deploy.js"

Python --- runs a script with python3, falling back to python:

[hooks.executor]
type = "python"
path = "data/scripts/deploy.py"

HTTP --- forwards to an endpoint:

[hooks.executor]
type = "http"
method = "POST"
url = "https://api.example.com/deploy"
headers = { Authorization = "Bearer token" }
body = '{"ref": "main"}'
follow_redirects = true

The Docker image includes Node.js and Python. Outside Docker, JavaScript hooks require node on PATH, and Python hooks require python3 or python.

Shell commands receive the raw payload in SENDWORD_PAYLOAD. Script, JavaScript, and Python executors also receive SENDWORD_PAYLOAD, flattened payload fields as SENDWORD_FIELD_*, and a payload.json file in the execution log directory. In Node.js, read these through process.env; in Python, read them through os.environ.

console.log(process.env.SENDWORD_PAYLOAD);
console.log(process.env.SENDWORD_FIELD_ACTION);
import os

print(os.environ["SENDWORD_PAYLOAD"])
print(os.environ.get("SENDWORD_FIELD_ACTION", ""))

Webhook authentication

Bearer token:

[hooks.auth]
mode = "bearer"
token = "your-secret-token"

Send as: Authorization: Bearer your-secret-token

HMAC-SHA256:

[hooks.auth]
mode = "hmac"
header = "X-Hub-Signature-256"
algorithm = "sha256"
secret = "your-hmac-secret"

Compatible with GitHub webhook signatures.

Trigger rules

Control when a hook fires:

[hooks.trigger_rules]
cooldown = "60s"  # minimum time between executions

# Only fire when payload matches
payload_filters = [
  { field = "action", operator = "equals", value = "deploy" },
  { field = "environment", operator = "contains", value = "prod" },
  { field = "tag", operator = "regex", value = "^v\\d+\\.\\d+\\.\\d+$" },
  { field = "metadata.priority", operator = "gte", value = "5" },
]

# Rate limit triggers
[hooks.trigger_rules.rate_limit]
max_requests = 10
window = "1h"

# Only allow during business hours
[[hooks.trigger_rules.time_windows]]
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
start_time = "09:00"
end_time = "17:00"

Filter operators: equals, not_equals, contains, regex, exists, gt, lt, gte, lte.

Execution barriers

Prevent conflicting concurrent executions:

# Mutex: only one execution at a time, others are rejected
[hooks.concurrency]
mode = "mutex"

# Queue: executions wait in line
[hooks.concurrency]
mode = "queue"
queue_depth = 10

Gate hooks behind human approval:

[hooks.approval]
required = true
timeout = "30m"  # optional, auto-reject after timeout

Environment variable overrides

Every config field can be set via environment variables:

SENDWORD_SERVER__PORT=9090
SENDWORD_DATABASE__PATH=/var/lib/sendword/db.sqlite
SENDWORD_AUTH__SESSION_LIFETIME=48h
SENDWORD_DEFAULTS__TIMEOUT=60s

CLI

sendword [COMMAND]

Commands:
  serve     Start the web server (default)
  export    Export current config as JSON to stdout
  import    Import config from a JSON file
  user      User management
  backup    Backup management
  restore   Restore from a backup

Examples

# Start the server
sendword serve

# Export config for version control or migration
sendword export > config-backup.json

# Import config from JSON
sendword import config.json

# Create a user
sendword user create --email admin@example.com

# Create a backup
sendword backup create

# List backups
sendword backup list

# Restore from backup
sendword restore --from backups/2026-04-30.tar.gz --output restored/

Triggering hooks

Send an HTTP POST to /hook/<slug>:

# Simple trigger
curl -X POST http://localhost:8080/hook/deploy-app

# With payload
curl -X POST http://localhost:8080/hook/deploy-app \
  -H "Content-Type: application/json" \
  -d '{"action": "deploy", "environment": "production"}'

# With bearer auth
curl -X POST http://localhost:8080/hook/deploy-app \
  -H "Authorization: Bearer secret-deploy-token" \
  -d '{"action": "deploy"}'

Development

sendword uses Nix flakes for development environment management and just as a command runner.

just          # list available commands
just run      # cargo run
just check    # cargo check
just test     # cargo test
just clippy   # cargo clippy -- -D warnings
just fmt      # cargo fmt
just watch    # bacon (file watcher)
just build    # cargo build --release

The web UI is rendered by typed Askama views in src/views/ that compose the published wavefunk-ui component and app-shell crate. Cargo.toml is the source of truth for the crates.io wavefunk-ui version; for local co-development, use the gitignored .cargo/config.toml path override to point Cargo at ../ui while preserving the published dependency metadata.

wavefunk-ui serves shared CSS, fonts, HTMX, SSE, and Wave Funk JavaScript under /static/wavefunk. Sendword only vendors app-specific behavior under static/js/sendword.js; the old MiniJinja template directory and local Wave Funk CSS/font copies are no longer part of the project.

Database

just migrate          # run pending migrations
just migrate-new NAME # create a new migration
just sqlx-prepare     # prepare sqlx offline queries
just sqlx-reset       # reset database and re-run migrations

Tech stack

Layer Choice
Language Rust (nightly)
Async runtime Tokio
Web framework Axum
Database SQLite via SQLx
Templating Askama
Frontend HTMX + wavefunk-ui
UI assets wavefunk-ui asset router plus static/js/sendword.js
Config Figment (TOML + JSON + env)

License

See LICENSE for details.