Iris transports things. Rite reacts to things.
Rite is a self-hostable, minimal event-to-action runtime. It receives authenticated events, normalizes them, matches TOML-configured handlers, and forwards matching events to actions.
GitHub webhook ──> Rite ──> configured HTTP action
Uptime Kuma webhook ──> Rite ──> configured HTTP action
Iris event ──> Rite ──> configured HTTP action
Create rite.toml:
[[rites]]
name = "github-pr-opened"
source = "github"
match = { event_type = "pull_request", action = "opened" }
action = { type = "http_post", url = "https://example.test/hooks/pr" }
[sources.iris]
enabled = true
base_url = "http://127.0.0.1:3000"
# Required when this ingress source is enabled.
[sources.uptime_kuma]
enabled = true
secret = "change-me"Run it:
RITE_GITHUB_WEBHOOK_SECRET=change-me cargo run -p rite-cli -- --config rite.tomlEndpoints:
GET /health— returnsokGET /sources— configured source adaptersPOST /event/github— authenticated GitHub webhook ingress usingX-Hub-Signature-256POST /event/uptime_kuma— authenticated Uptime Kuma webhook ingress using base64SignatureHMAC-SHA256
Rite's agent-facing read operations are generated from api/operations.yaml and run through the
same execute_operation dispatch as HTTP. They load the local configuration and print JSON, so
no HTTP server or webhook secret is required:
cargo run -p rite-cli -- list-sources --config rite.example.toml
cargo run -p rite-cli -- list-handlers --config rite.example.toml
cargo run -p rite-cli -- get-handler github-pr-opened --config rite.example.tomlrite serve --config rite.toml --github-webhook-secret "$RITE_GITHUB_WEBHOOK_SECRET" explicitly
starts the server; omitting the subcommand remains equivalent for compatibility. The generated
generated/mcp.json declares the same read operations for MCP hosts over the existing HTTP
endpoints: GET /sources, GET /handlers, and GET /handlers/{handler_id}. No separate server
surface is needed.
When enabled, the Iris source subscribes to GET /v1/events in the background. Set
sources.iris.api_token (or RITE_IRIS_API_TOKEN in Docker) when Iris requires bearer
authentication; Rite sends it only as the subscription's Authorization: Bearer header, including
on reconnects. Without a token Rite warns at startup so legacy unauthenticated Iris deployments
remain supported. It reconnects with exponential backoff (1–60 seconds), so Rite remains healthy
while Iris is unavailable. Iris messages become source = "iris" events. event_type is the Iris message kind and metadata
contains provider, source_id, sender, kind, iris_metadata, and the complete original
Iris message under message for future matching needs.
Published images are available from GitHub Container Registry after a release tag:
ghcr.io/techgodhq/rite:<version> (or :latest). Supply a webhook secret and,
when Rite should subscribe to Iris events, its private Iris URL:
docker run --rm \
--name rite \
--publish 127.0.0.1:8080:8080 \
--env RITE_GITHUB_WEBHOOK_SECRET="${RITE_GITHUB_WEBHOOK_SECRET}" \
--env RITE_IRIS_BASE_URL="http://iris.internal:9876" \
--env RITE_IRIS_API_TOKEN="${RITE_IRIS_API_TOKEN}" \
ghcr.io/techgodhq/rite:latestRITE_GITHUB_WEBHOOK_SECRET is required for authenticated GitHub webhook
ingress. Omit RITE_IRIS_BASE_URL when you do not want an Iris subscription. Set
RITE_IRIS_API_TOKEN whenever that Iris instance has IRIS_API_TOKEN configured; it is not
written to logs.
For a complete private-network example with both services, use the Iris
repository's deploy/docker-compose.yml.
http_post forwards the normalized event as JSON by default. A handler may
instead set body_template to render a deterministic text body using
{{event_type}}, {{action}}, {{title}}, {{body}}, or nested event
metadata such as {{metadata.provider}}; missing fields render empty.
Optional headers are forwarded verbatim. Templated bodies default to
Content-Type: text/plain unless that header is explicitly set. This remains
a generic HTTP action: Discord, Slack, and other webhook targets do not add
provider-specific Rite action types.
[[rites]]
name = "deploy-alert"
source = "iris"
match = { body_contains = "URGENT" }
action = { type = "http_post", url = "https://hooks.example.test/alerts", headers = { "content-type" = "application/json" }, body_template = "{\"content\":\"{{title}}: {{body}}\"}" }Flat match tables AND every entry. For OR or negation, compose conditions
with all_of, any_of, and not — arbitrarily nestable, with leaf keys
keeping exactly the semantics above:
[[rites]]
name = "page-on-critical"
source = "iris"
match = { any_of = [{ severity = "critical" }, { body_contains = "URGENT" }] }
action = { type = "http_post", url = "https://hooks.example.test/pages" }
[[rites]]
name = "real-prs-only"
source = "github"
match = { all_of = [{ event_type = "pull_request" }, { not = { draft = true } }] }
action = { type = "http_post", url = "https://hooks.example.test/reviews" }all_of and any_of take arrays of condition tables and reject empty
arrays; not takes a single condition table. A table mixing operators with
other keys — or using two operators as siblings — is rejected at config load
with an actionable error. A scalar under an operator-named key (e.g.
not = "draft") is an ordinary metadata lookup, exactly as before: the
operator only applies to table-shaped values, so existing configurations
never change meaning.
cargo build --all-targets
cargo test --all-targets
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --checkRite normalizes GitHub push and pull_request payloads plus Uptime Kuma heartbeats, matches configured handlers, and executes http_post actions with the normalized event as JSON. Uptime Kuma heartbeats require a configured secret; status maps deterministically to up (info), down (critical), pending (warning), or maintenance (info). Matching-safe metadata includes monitor_name, monitor_id, status, plus present URL/timing fields; Kuma's msg becomes the optional event body.