-
Notifications
You must be signed in to change notification settings - Fork 0
Writing Modules
Build your own module — a thread to weave into the MuxCore fabric. This guide walks through creating a sidecar module from nothing: declaring its role, connecting to the gRPC mesh, discovering other modules, and communicating through events.
You need:
- Go 1.21 or later
- A GitHub account (to publish the module)
- Understanding of Core Concepts and the Module System
- A running MuxCore instance (
muxcored) for testing
Every module is a standalone binary — not compiled into core. Core spawns it as a child process; the module connects to the gRPC mesh and registers at runtime.
my-module/
├── cmd/
│ └── module/
│ └── main.go ← sidecar entry point (gRPC registration, signal handling)
├── internal/
│ ├── module.go ← contracts.Module implementation
│ └── ... ← module-specific logic
├── go.mod ← depends on core + contract repos
├── go.sum
└── muxcore.json ← marketplace metadata
Reference implementation: downloader-qbittorrent.
mkdir my-module && cd my-module
go mod init github.com/yourname/my-module
go get github.com/Muxcore-Media/core
go get github.com/Muxcore-Media/contracts-downloader # if building a downloaderEvery module implements the contracts.Module lifecycle interface:
package internal
import (
"context"
"github.com/Muxcore-Media/core/pkg/contracts"
dl "github.com/Muxcore-Media/contracts-downloader"
)
type Module struct {
id string
meshAddr string
conn *grpc.ClientConn
// ... module-specific fields
}
func NewModule(moduleID, meshAddr string, conn *grpc.ClientConn) *Module {
return &Module{id: moduleID, meshAddr: meshAddr, conn: conn}
}
func (m *Module) Info() contracts.ModuleInfo {
return contracts.ModuleInfo{
ID: "downloader-qbittorrent",
Name: "qBittorrent Downloader",
Version: "1.0.0",
Roles: []string{"downloader"},
Description: "Bridges MuxCore to qBittorrent via its Web API",
Author: "MuxCore",
Capabilities: []string{"downloader.torrent", "downloader.qbittorrent"},
Contracts: []contracts.ContractDeclaration{
{
Repo: "github.com/Muxcore-Media/contracts-downloader",
Version: "v1.0.0",
Interface: "Downloader",
},
},
DependsOn: []string{},
}
}
func (m *Module) Init(ctx context.Context) error {
// Discover services via DiscoveryService gRPC
discClient := discoveryv1.NewDiscoveryServiceClient(m.conn)
resp, _ := discClient.FindByCapability(ctx, &discoveryv1.FindByCapabilityRequest{
Capability: "storage",
})
if len(resp.Modules) > 0 {
// Storage backend found — set up storage client
}
return nil
}
func (m *Module) Start(ctx context.Context) error {
// Begin operations: start pollers, connect to external services
return nil
}
func (m *Module) Stop(ctx context.Context) error {
// Graceful shutdown: stop pollers, close connections
return nil
}
func (m *Module) Health(ctx context.Context) error {
// Check connectivity to external dependencies
return nil
}Create cmd/module/main.go — this is the binary that core spawns:
package main
import (
"context"
"encoding/json"
"flag"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/yourname/my-module/internal"
modulev1 "github.com/Muxcore-Media/core/proto/gen/muxcore/module/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
meshAddr := flag.String("muxcore-mesh-addr", "localhost:9090", "gRPC address of the MuxCore mesh")
moduleID := flag.String("muxcore-module-id", "my-module", "Module identifier")
flag.Parse()
// Connect to core mesh with retry
var conn *grpc.ClientConn
var err error
for attempt := 0; attempt < 5; attempt++ {
conn, err = grpc.NewClient(*meshAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err == nil {
break
}
if attempt < 4 {
slog.Warn("mesh connection failed, retrying", "attempt", attempt+1, "error", err)
time.Sleep(time.Duration(attempt+1) * time.Second)
}
}
if err != nil {
slog.Error("failed to connect to mesh", "error", err)
os.Exit(1)
}
defer conn.Close()
// Register with core
regClient := modulev1.NewModuleRegistrationClient(conn)
mod := internal.NewModule(*moduleID, *meshAddr, conn)
info := mod.Info()
infoJSON, _ := json.Marshal(info)
resp, err := regClient.Register(context.Background(), &modulev1.RegisterRequest{
ModuleId: *moduleID,
InfoJson: infoJSON,
MeshAddr: *meshAddr,
})
if err != nil || !resp.Accepted {
slog.Error("registration failed", "error", err, "reason", resp.Error)
os.Exit(1)
}
// Start the module
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
mod.Init(ctx)
mod.Start(ctx)
<-ctx.Done() // Wait for shutdown signal
mod.Stop(context.Background())
regClient.Unregister(context.Background(), &modulev1.UnregisterRequest{ModuleId: *moduleID})
}Every sidecar binary must:
- Accept
--muxcore-mesh-addr(default:localhost:9090) - Accept
--muxcore-module-id(its identifier) - Connect to the gRPC mesh
- Call
ModuleRegistration.Register()with JSON-serializedModuleInfo - Respond to SIGTERM/SIGINT for graceful shutdown
- Call
ModuleRegistration.Unregister()on exit
Create muxcore.json in the repo root:
{
"name": "My Module",
"description": "Does something useful",
"version": "1.0.0",
"author": "Your Name",
"roles": ["example"],
"capabilities": ["example.basic"],
"contracts": [
{"repo": "github.com/Muxcore-Media/contracts-downloader", "version": "v1.0.0", "interface": "Downloader"}
]
}Find modules via the DiscoveryService gRPC:
disc := discoveryv1.NewDiscoveryServiceClient(conn)
// By role
resp, _ := disc.FindByRole(ctx, &discoveryv1.FindByRoleRequest{Role: "downloader"})
for _, mod := range resp.Modules {
// mod.Id, mod.Roles, mod.Capabilities
}
// By capability
resp, _ := disc.FindByCapability(ctx, &discoveryv1.FindByCapabilityRequest{Capability: "secrets"})
// Direct resolve
resp, _ := disc.Resolve(ctx, &discoveryv1.ResolveRequest{ModuleId: "downloader-qbittorrent"})import dl "github.com/Muxcore-Media/contracts-downloader"
payload, _ := json.Marshal(dl.DownloadCompletedPayload{
DownloadID: id,
Title: "Movie Title",
})
events := eventsv1.NewEventServiceClient(conn)
events.Publish(ctx, &eventsv1.PublishRequest{
Event: &eventsv1.Event{
Type: dl.EventDownloadCompleted,
Source: "my-module",
Payload: payload,
},
})Event types and payloads are defined in the domain contract repos. See Event System for the full event model.
store := storagev1.NewStorageServiceClient(conn)
// Upload (client-streaming)
stream, _ := store.Put(ctx)
stream.Send(&storagev1.PutRequest{Key: "path/to/object", TotalSize: size})
for _, chunk := range chunks {
stream.Send(&storagev1.PutRequest{Chunk: chunk})
}
resp, _ := stream.CloseAndRecv()
// Download (server-streaming)
getStream, _ := store.Get(ctx, &storagev1.GetRequest{Key: "path/to/object"})
for {
resp, err := getStream.Recv()
if err == io.EOF { break }
// process resp.Chunk
}
// Metadata
stat, _ := store.Stat(ctx, &storagev1.StatRequest{Key: "path/to/object"})
caps, _ := store.Capabilities(ctx, &storagev1.CapabilitiesRequest{})For request/response patterns, use the ModuleMesh:
mesh := meshv1.NewModuleMeshClient(conn)
resp, _ := mesh.Call(ctx, &meshv1.CallRequest{
TargetModule: "transcoder-ffmpeg",
Method: "Status",
Payload: jsonPayload,
})import dl "github.com/Muxcore-Media/contracts-downloader"
func (m *Module) Add(ctx context.Context, task dl.DownloadTask) (string, error) {
// task.MagnetURI, task.TorrentURL, task.Meta carry download details
// Bridge to external download client (qBittorrent, usenet, etc.)
return downloadID, nil
}See downloader-qbittorrent for a complete reference implementation — it bridges MuxCore to qBittorrent's Web API, polls torrent state, emits events, and streams files.
func (m *Module) Put(ctx context.Context, key string, data io.Reader, size int64) (int64, error) { ... }
func (m *Module) Get(ctx context.Context, key string) (io.ReadCloser, error) { ... }
func (m *Module) Stat(ctx context.Context, key string) (contracts.ObjectInfo, error) { ... }
// Optional capability interfaces
func (m *Module) Hardlink(ctx context.Context, src, dst string) error { ... }A storage module registers with role "storage" and announces capabilities like "hardlinkable" and "streamable". Other modules discover it via FindByCapability("storage") and use the StorageService gRPC.
Unit tests mock the external services (HTTP servers, gRPC backends). Integration tests need a running muxcored instance.
func TestMyClient(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Return expected API responses
}))
defer srv.Close()
client := NewClient(srv.URL, "user", "pass", 5*time.Second)
ctx := context.Background()
client.Login(ctx)
// ... assertions
}# Start core in insecure dev mode
MUXCORE_INSECURE_DISABLE_TLS=true muxcored &
# Build and run module
go build -o my-module ./cmd/module
./my-module --muxcore-mesh-addr localhost:9090 --muxcore-module-id my-module# docker-compose.test.yml
services:
qbittorrent:
image: linuxserver/qbittorrent:latest
ports: ["8083:8080"]
volumes: ["./testdata/downloads:/downloads"]Before publishing:
-
go.moddepends ongithub.com/Muxcore-Media/coreplus domain contract repos - Sidecar entry point at
cmd/module/main.go - Accepts
--muxcore-mesh-addrand--muxcore-module-idflags - Calls
ModuleRegistration.Register()at startup - Calls
ModuleRegistration.Unregister()at shutdown - Handles SIGTERM/SIGINT for graceful shutdown
- Implements
contracts.Module(Info, Init, Start, Stop, Health) - Implements domain contracts from appropriate
contracts-*repos -
Contractsdeclared inInfo().Contracts -
muxcore.jsonat repo root withcontractsandcapabilities - All external services discovered via
DiscoveryService.FindByCapability(), never assumed - Works gracefully when services are unavailable (degraded state)
-
Metaused for module-specific data instead of assuming core changes
- Contracts Reference — every interface in detail
- Module System — all module types and discovery
- Event System — how modules communicate through events
- downloader-qbittorrent — complete reference implementation