Outbound webhooks and MCP subscriptions

Forge's internal Signals were always there — but nothing outside could react. M11 adds two complementary delivery mechanisms on top of the same Signal: outbound webhooks for pipelines and integrations, MCP subscriptions for agents.

Forge's internal Signals were always there. A post publishes, a story archives, a doc page goes live. The Signal fires, the database reflects it. But nothing outside Forge could react. No webhook. No notification. Just silence.

M11 changes that. Two complementary delivery mechanisms, one shared Signal.

Two layers

The delivery engine is intentionally generic. An OutboundJob holds a target URL, a payload, a secret, and retry state. A DeliveryLog records every attempt. A bounded worker pool pulls due jobs, signs the payload, POSTs, and logs the result. The engine knows nothing about content types or lifecycle events.

On top of that sits a thin binding layer: when a Signal fires, enqueue a job. That is the entire interface between Forge's domain logic and the delivery engine.

This split matters because the delivery engine will be reused by forge-social. The mechanics of reliable outbound HTTP delivery should not be reimplemented per use case.

WebhookEndpoint as a content type

Webhook configuration is registered as a content type via App.Register, not as entries in a config file. This fits the existing pattern: MCP tools are derived automatically, and the admin API works without additional code.

The deeper reason is the agent provisioning scenario. In the Forge vision, provisioning agents operate via MCP, not the filesystem. An agent that needs to register a webhook endpoint must be able to do so through the MCP interface. Putting configuration in a file would make that impossible.

Secrets that can be decrypted

TokenStore hashes tokens. You can verify a token, but you cannot recover the original value. Webhook secrets cannot follow the same pattern: the delivery engine needs the raw secret to compute the HMAC signature on every outbound request.

Webhook secrets are stored with AES-GCM encryption using Config.Secret as key material. Decryptable at delivery time. The secret is returned once at creation and never again through the API.

SSRF protection at registration

Validating the target URL at delivery time is too late. An agent operating under prompt injection could register an endpoint pointing at an internal service and wait for the next Signal to trigger delivery.

Forge validates the target URL when the endpoint is registered: reject HTTP, reject private IP ranges, reject localhost and .local hostnames. The job is never enqueued for an invalid target. The attack surface closes at the point where the attacker has the least leverage.

Testable by design

The worker pool accepts an injectable deliverFunc. Tests swap in a function that records calls without making network requests. A Clock interface replaces time.Now and time.Sleep in the circuit breaker and backoff logic. The test suite exercises retry sequences and circuit breaker state transitions without waiting for real time to pass.

MCP subscriptions

Webhooks are the right mechanism for integration platforms and custom backends. For agents maintaining a persistent MCP connection to Forge, there is a better path.

forge-mcp declares resources.subscribe: true in server capabilities. Content items are exposed as MCP resources at forge://{type}/{slug}. When a Signal fires, connected subscribers receive a notifications/resources/updated notification for the affected resource. The agent reacts immediately. No polling, no missed events, no additional infrastructure.

The subscription registry is in-memory per connection. If forge-mcp restarts, clients re-subscribe. Acceptable for v1.

One Signal, two paths

The same Signal that has always fired now has two listeners. One enqueues an outbound HTTP job. One walks the subscription registry and pushes MCP notifications. Neither knows about the other.

This is what "Forge is the state, not the executor" looks like at runtime.