webhooks

package
v1.801.414 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package webhooks is how your app hears about events: register an endpoint, pick the events, get each one delivered and signed.

It is the platform-global webhook layer (HIP-0106) — ONE registry plus ONE dispatcher that delivers ANY event on the platform bus to org-registered HTTP subscribers. It supersedes commerce's local, billing-scoped delivery path — the webhook surface is /v1/webhooks, top-level, never under /v1/billing, and it fans out EVERY platform event, not just commerce's.

TWO ORTHOGONAL HALVES.

  • REGISTRY (api.go, store.go) — /v1/webhooks CRUD. An org manages ONLY its own endpoints; each org's registry is a physically separate {DataDir}/orgs/{slug}/ webhooks.db (the apps/books per-org SQLite idiom via cloud.OrgStore), so one tenant can never read or mutate another's. The org is the VALIDATED principal (principal.Org, gateway-minted X-Org-Id), exactly like apps/notify — 401 for an unauthenticated caller.
  • DISPATCHER (dispatch.go, match.go) — a durable JetStream consumer on the platform bus (apps/pubsub), over the streams dispatch.go names: COMMERCE (commerce.>, owned by hanzoai/commerce) and the event plane (event.>, owned by apps/analytics, which publishes it). It resolves each event's org from the envelope, matches ONLY that org's active subscriptions (NATS subject-wildcard semantics), and POSTs each match with a fresh HMAC-SHA256 signature and a bounded retry ladder. Org isolation is by construction: the store lookup is per-org, so B's endpoint can never receive A's event.

CONSUMER ONLY. This package publishes nothing and owns no stream. It once held the publish half of the event plane too — its own stream name, its own envelope — which made two subsystems owners of one subject space; JetStream answers that with "subjects overlap with an existing stream", so the second owner to arrive delivers nothing, on every stream, forever. The producer lives with the data now (analytics.PublishEvents).

FAIL-SOFT MOUNT. The registry always mounts. The dispatcher is best-effort: a down bus ⇒ background reconnect-retry. A messaging fault never crashes the process.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount opens the per-org registry stores, wires /v1/webhooks, and starts the bus dispatcher (fail-soft). It never returns an error for a bus problem — only for a genuinely unusable Deps — so a messaging fault can never abort the binary's boot.

func Shutdown

func Shutdown(_ context.Context) error

Shutdown stops the dispatcher (draining its workers) and closes every open per-org store. Idempotent.

Types

type DeliveryRow

type DeliveryRow struct {
	EndpointID string `json:"endpoint"`
	DeliveryID string `json:"delivery"`
	Subject    string `json:"subject"`
	Attempt    int    `json:"attempt"`
	Status     string `json:"status"`
	HTTPStatus int    `json:"httpStatus"`
	Error      string `json:"error,omitempty"`
	DurationMs int64  `json:"durationMs"`
	Created    string `json:"created"`
}

DeliveryRow is one recorded delivery attempt: the /:id/deliveries wire model AND the stored row. One attempt-group (a single event → one endpoint) writes one row per attempt, all sharing a Delivery id; Status is "ok" | "retrying" | "failed". HTTPStatus is 0 on a network/timeout error, Error is empty on success.

type Endpoint

type Endpoint struct {
	ID          string   `json:"id"`
	Org         string   `json:"org"`
	URL         string   `json:"url"`
	Events      []string `json:"events"`
	Secret      string   `json:"secret,omitempty"`
	Status      string   `json:"status"`
	Description string   `json:"description,omitempty"`
	CreatedAt   string   `json:"created"`
	UpdatedAt   string   `json:"updated"`

	// Deliveries7d / Failures7d are cheap usage counters computed from the delivery log
	// over usageWindow (not stored columns) and populated ONLY on list/get. They are 0
	// when there is no delivery history — never omitempty, so the console always sees them.
	Deliveries7d int `json:"deliveries7d"`
	Failures7d   int `json:"failures7d"`
}

Endpoint is one registered webhook subscriber. It is BOTH the wire model and the stored row. Secret is returned ONLY on create (json omitempty + cleared elsewhere), so the signing key leaves the server exactly once.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL