Building modules

External modules extend Trove without recompiling the core. See spec §8 and concepts/modules.md.

Status: Module runtime is Supported for local source modules — see planning/module-runtime.md.

Layout

Install under a configured search path:

~/.local/lib/trove/modules/my-source/
    manifest.toml
    module               # executable

For local development, make build compiles bin/trove (with http-ingest, mcp-query, and type-catalog built in) and builds optional first-party modules to modules/mqtt-source/, modules/telegram-source/, and modules/capture-classifier/. Standalone copies of the HTTP and MCP modules are also emitted under modules/http-ingest/ and modules/mcp-query/ for module-only iteration. Point [modules].paths at the repo modules/ directory when testing external modules; built-in ingest and MCP work with an empty paths list.

Manifest

name     = "my-source"
version  = "1.0"
kind     = "source"
provides = ["trove://type/my-source/event/received/1", "trove://type/my-source/*"]

[[types]]
name    = "my-source.event.received"
version = 1
schema  = "types/received.ttd.json"

Each [[types]] entry points at a Trove Type Definition (TTD) JSON file relative to the module directory. The TTD envelope uses RFC 8927 JTD in definition and a trove://type/... $id that must match a provides pattern.

Revision-routing processor example:

name     = "embedder"
version  = "0.1.0"
kind     = "processor"
consumes = ["trove://type/note/*"]
provides = ["trove://type/note/embedding/generated/1"]

Sink example:

name     = "webhook-sink"
version  = "2.0"
kind     = "sink"
consumes = ["trove://type/note/created/1"]

kind is source, processor, or sink.

Kind provides consumes
source required forbidden
processor (revision-routing) required when emitting derived revisions required
processor (HTTP-only) forbidden forbidden; use [[http.routes]]
processor (CLI/MCP-only) forbidden forbidden; use [[cli.commands]] and/or [[mcp.tools]]
processor (auth-only) forbidden forbidden; use [[auth.validators]]
sink forbidden required

Each pattern is an exact trove://type/... URI or a glob (trove://type/note/*, trove://type/mqtt/*/received/*). Bare * is not allowed. Per-module listen addresses are rejected — register [[http.routes]] and let the core HTTP gateway listen instead.

[[types]] entries declare payload contracts for types the module provides. At startup the core loads TTD files into the type catalog and validates payloads on every emit, stamping schema_ref on success. Legacy [schemas] JSON Schema entries are not supported.

Module-specific keys such as listen are read by the module binary; the core parser ignores unknown fields.

Module contract

Modules run as subprocesses of the single trove process. When the parent starts a module, it passes a Core handle — your connection back to the parent for journal writes, blob storage, and journal reads:

func (m *myModule) Run(ctx context.Context, core trovemodule.Core) error {
    _, err := core.AppendRevision(ctx, &troverpc.AppendRevisionRequest{ ... })
    return err
}

Use trovemodule.Serve to register the module. Optional interfaces:

  • HTTPHandler — serve HTTP routes declared in the manifest
  • AuthHandler — validate gateway requests for [[auth.validators]] entries
  • MCPToolHandler — handle MCP tools declared in [[mcp.tools]]
  • CLIHandler — handle CLI commands declared in [[cli.commands]]
  • RevisionProcessorProcess(revision, dispatch) for revision-routing processors
  • RevisionSinkHandle(revision, dispatch) for sinks
  • HealthChecker — report liveness to the parent

Revision-routing processors and sinks implement Run with trovemodule.WaitCore when they do not stream from Run themselves. The parent passes a DispatchContext with root_id and seen module names for loop prevention. Routed revisions include operation (apply or delete); return early from Process / Handle when your module should ignore an operation.

The parent enforces ingest policy on core.AppendRevision and on derived revisions returned from Process. Modules do not open trove.db or the blob directory directly.

Module-specific config

Broker addresses, topics, API tokens, and similar settings belong in the module's own config (alongside or inside manifest.toml), not in the core TOML.

Optionally override module settings from trove.toml without editing the module directory:

[modules.settings.mqtt-source]
broker = "tcp://mosquitto:1883"

[modules.config]
telegram-source = "/etc/trove/telegram.toml"

See Configuration. Modules load overlays through trovemodule.LoadModuleConfig (merges manifest + TROVE_MODULE_SETTINGS when set by the parent).

Examples

Module Location Planning page
HTTP gateway modules/http-gateway/ http-gateway
HTTP ingest modules/http-ingest/ http-ingest
Capture classifier modules/capture-classifier/ deferred-capture
MCP query modules/mcp-query/ mcp-query
MQTT source modules/mqtt-source/ mqtt-source
Telegram source modules/telegram-source/ telegram-source
Home Assistant external ha-source

HTTP ingest

After make build, add the repo modules/ directory to [modules].paths and start trove. POST JSON to http://localhost:8080/ingest/shortcuts (default listen address). The :source path segment becomes the event source field; optional type, time, and blob_ref keys in the JSON body override event metadata. Default request body limit is 10 MiB (max_body_bytes in manifest). Allowed client type values are controlled by provides in the module manifest (for example trove://type/shortcuts/* for Shortcuts). Disallowed types and payload validation failures return 400 Bad Request with an error message.

For large attachments, do not inline bytes in JSON. Upload content via PUT /blobs and reference it with blob_ref on the ingest payload.

See iOS Shortcuts for importable capture Shortcuts that POST to this endpoint.

MQTT source

After make build, configure broker and topics in modules/mqtt-source/manifest.toml (default broker tcp://localhost:1883, topics ["home/#"]). Add modules/ to [modules].paths and start trove. Each MQTT message becomes a journal event with type trove://type/mqtt/message/received/1, source set to the topic, and payload.metadata.topic preserving the original MQTT topic, with the MQTT body in payload.message (JSON) or payload.raw (non-JSON).

Telegram source

After make build, set TELEGRAM_BOT_TOKEN and your chat ID in modules/telegram-source/manifest.toml. Add modules/ to [modules].paths and start trove. Send a message to your bot; it replies with a capture event ID and type buttons. See Telegram getting started.

Publishing

No central registry in v0 — copy the module directory into a search path on the host running Trove.