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]] - RevisionProcessor —
Process(revision, dispatch)for revision-routing processors - RevisionSink —
Handle(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.