-
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 module from nothing: declaring its role, accepting the platform services, 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 module is a Go package that implements one required interface and registers itself at startup:
my-module/
├── go.mod ← depends on core + any contract repos
├── module.go ← your implementation
└── muxcore.json ← marketplace metadata
mkdir my-module && cd my-module
go mod init github.com/yourname/my-module
go get github.com/Muxcore-Media/corepackage mymodule
import (
"context"
"github.com/Muxcore-Media/core/pkg/contracts"
)
// Register with the platform at startup
func init() {
contracts.Register(func(fabric contracts.Fabric) contracts.Module {
return NewModule(fabric.EventBus, fabric.Registry)
})
}
type Module struct {
bus contracts.EventBus
reg contracts.Registry
}
func NewModule(bus contracts.EventBus, reg contracts.Registry) *Module {
return &Module{bus: bus, reg: reg}
}
// Required: describe this module
func (m *Module) Info() contracts.ModuleInfo {
return contracts.ModuleInfo{
ID: "my-module",
Name: "My Module",
Version: "1.0.0",
Roles: []string{"example"},
Description: "Does something useful",
Author: "Your Name",
Capabilities: []string{"example.basic"},
Contracts: []contracts.ContractDeclaration{
{Repo: "github.com/Muxcore-Media/core", Version: "v1.0.0", Interface: "Module"},
},
}
}
// Required: lifecycle methods
func (m *Module) Init(ctx context.Context) error { return nil }
func (m *Module) Start(ctx context.Context) error { return nil }
func (m *Module) Stop(ctx context.Context) error { return nil }
func (m *Module) Health(ctx context.Context) error { return nil }Create muxcore.json in the repo root:
{
"name": "My Module",
"description": "Does something useful",
"version": "1.0.0",
"author": "Your Name",
"role": "example",
"capabilities": ["example.basic"],
"contracts": [
{"repo": "github.com/Muxcore-Media/core", "version": "v1.0.0", "interface": "Module"}
]
}That's it — a working module. It doesn't do anything yet, but it registers, initializes, starts, and stops cleanly.
Every module receives a Fabric struct with eight services. You use what you need:
func NewModule(fabric contracts.Fabric) *Module {
return &Module{
bus: fabric.EventBus, // send and receive events
reg: fabric.Registry, // find other modules
storage: fabric.Storage, // store and retrieve files
routes: fabric.Routes, // register HTTP endpoints
mesh: fabric.Mesh, // call other modules directly
cluster: fabric.Cluster, // cluster membership (nil if single-node)
workers: fabric.WorkerPool, // schedule distributed tasks
audit: fabric.Audit, // record security events
}
}Find modules by role or capability:
// Broad: find all downloaders
entries := fabric.Registry.FindByRole("downloader")
// Specific: find by capability
entries := fabric.Registry.FindByCapability("secrets")
// Direct: resolve by ID
entry, err := fabric.Registry.Resolve("downloader-qbittorrent")Once you have an entry, type-assert to the interface you need:
import dl "github.com/Muxcore-Media/contracts-downloader"
entries := fabric.Registry.FindByRole("downloader")
for _, entry := range entries {
if d, ok := entry.Module.(dl.Downloader); ok {
d.Add(ctx, dl.DownloadTask{
MagnetURI: "magnet:?...",
Label: "My Download",
Meta: map[string]any{"priority": "high"},
})
}
}import dl "github.com/Muxcore-Media/contracts-downloader"
payload, _ := json.Marshal(dl.DownloadCompletedPayload{
DownloadID: id,
Title: "Movie Title",
})
fabric.EventBus.Publish(ctx, contracts.Event{
Type: dl.EventDownloadCompleted,
Source: "my-module",
Payload: payload,
})fabric.EventBus.Subscribe(ctx, dl.EventDownloadCompleted,
func(ctx context.Context, event contracts.Event) error {
var payload dl.DownloadCompletedPayload
json.Unmarshal(event.Payload, &payload)
// React to the download finishing
return nil
})For request/response patterns, use the mesh:
result, err := fabric.Mesh.Call(ctx, "transcoder-ffmpeg", "Status", jsonPayload)If a CallPolicyProvider is registered, the mesh enforces access control. Set your module's identity before calling:
ctx = contracts.WithCallerID(ctx, "my-module")
result, err := fabric.Mesh.Call(ctx, "target-module", "Method", payload)If other modules need to call yours, register a handler:
func (m *Module) Start(ctx context.Context) error {
m.fabric.Mesh.RegisterHandler("my-module", m)
return nil
}
func (m *Module) HandleCall(ctx context.Context, method string, payload []byte) ([]byte, error) {
switch method {
case "Status":
return json.Marshal(map[string]string{"status": "running"})
case "DoSomething":
// handle it
return json.Marshal(map[string]string{"result": "ok"})
default:
return nil, fmt.Errorf("unknown method: %s", method)
}
}Here's how to build modules for common roles. Each requires importing the appropriate domain contract repo.
import dl "github.com/Muxcore-Media/contracts-downloader"
// Implement the Downloader interface
func (m *Module) Add(ctx context.Context, task dl.DownloadTask) (string, error) {
// task.Meta carries module-specific data (magnet URIs, HTTP URLs, etc.)
magnet := task.Meta["magnet_uri"].(string)
// ... add to download client ...
return downloadID, nil
}
func (m *Module) Info() contracts.ModuleInfo {
return contracts.ModuleInfo{
ID: "downloader-qbittorrent",
Name: "qBittorrent Downloader",
Version: "1.0.0",
Roles: []string{"downloader"},
Capabilities: []string{"downloader.torrent"},
Contracts: []contracts.ContractDeclaration{
{Repo: "github.com/Muxcore-Media/contracts-downloader", Version: "v1.0.0", Interface: "Downloader"},
},
}
}import media "github.com/Muxcore-Media/contracts-media"
func (m *Module) List(ctx context.Context, mediaType media.MediaType, filter *media.MediaFilter, offset, limit int) ([]media.MediaObject, error) {
// filter.Meta carries media-type-specific filters (artist, album, BPM, etc.)
// Return media objects matching the filter
}
func (m *Module) Info() contracts.ModuleInfo {
return contracts.ModuleInfo{
ID: "media-manager-movies",
Name: "Movie Media Manager",
Version: "1.0.0",
Roles: []string{"media_manager"},
Capabilities: []string{"media.movie"},
Contracts: []contracts.ContractDeclaration{
{Repo: "github.com/Muxcore-Media/contracts-media", Version: "v1.0.0", Interface: "MediaLibrary"},
},
}
}// Implement StorageProvider (in core contracts)
func (m *Module) Put(ctx context.Context, key string, data io.Reader, size int64) error { ... }
func (m *Module) Get(ctx context.Context, key string) (io.ReadCloser, error) { ... }
func (m *Module) Delete(ctx context.Context, key string) error { ... }
func (m *Module) Move(ctx context.Context, src, dst string) error { ... }
func (m *Module) Exists(ctx context.Context, key string) (bool, error) { ... }
func (m *Module) Stat(ctx context.Context, key string) (contracts.ObjectInfo, error) { ... }
func (m *Module) List(ctx context.Context, prefix string) ([]contracts.ObjectInfo, error) { ... }
// Optional: implement capability interfaces for extra features
func (m *Module) Stream(ctx context.Context, key string, offset, length int64) (io.ReadCloser, error) { ... }
func (m *Module) AtomicMove(ctx context.Context, src, dst string) error { ... }
func (m *Module) Hardlink(ctx context.Context, src, dst string) error { ... }
func (m *Module) Info() contracts.ModuleInfo {
return contracts.ModuleInfo{
ID: "storage-local",
Name: "Local Filesystem Storage",
Version: "1.0.0",
Roles: []string{"storage"},
Capabilities: []string{"storage.local", "storage.posix"},
}
}entries := fabric.Registry.FindByCapability("secrets")
if len(entries) > 0 {
secrets := entries[0].Module.(contracts.SecretsProvider)
apiKey, _ := secrets.Get(ctx, "api_key")
}entries := fabric.Registry.FindByCapability("tracing")
if len(entries) > 0 {
tp := entries[0].Module.(contracts.TracingProvider)
ctx, span := tp.StartSpan(ctx, "my-operation")
defer span.End()
span.SetAttribute("key", "value")
}entries := fabric.Registry.FindByCapability("circuitbreaker")
if len(entries) > 0 {
cb := entries[0].Module.(contracts.CircuitBreaker)
err := cb.Execute(ctx, "upstream-service", func(ctx context.Context) error {
_, err := fabric.Mesh.Call(ctx, "target", "Method", payload)
return err
})
if errors.Is(err, contracts.ErrCircuitOpen) {
// Circuit is open — handle gracefully
}
}entries := fabric.Registry.FindByCapability("retry")
if len(entries) > 0 {
rp := entries[0].Module.(contracts.RetryProvider)
err := rp.Execute(ctx, contracts.RetryPolicy{
MaxAttempts: 5,
InitialDelay: 100 * time.Millisecond,
BackoffFactor: 2.0,
Jitter: true,
}, func(ctx context.Context) error {
return callUpstream(ctx)
})
}entries := fabric.Registry.FindByCapability("idempotency")
if len(entries) > 0 {
ip := entries[0].Module.(contracts.IdempotencyProvider)
isNew, err := ip.Store(ctx, requestID, result, 24*time.Hour)
if !isNew {
cached, _ := ip.Result(ctx, requestID)
return cached, nil // Already processed
}
}entries := fabric.Registry.FindByCapability("feature.flags")
enabled := false
if len(entries) > 0 {
ff := entries[0].Module.(contracts.FeatureFlagProvider)
enabled = ff.IsEnabled(ctx, "new-recommendation-engine", false)
}entries := fabric.Registry.FindByCapability("encryption")
if len(entries) > 0 {
ep := entries[0].Module.(contracts.EncryptionProvider)
ciphertext, _ := ep.Encrypt(ctx, sensitiveData)
fabric.Storage.Put(ctx, "sensitive/key", ciphertext)
}entries := fabric.Registry.FindByCapability("distributed.lock")
if len(entries) > 0 {
lp := entries[0].Module.(contracts.DistributedLockProvider)
handle, err := lp.Acquire(ctx, "migration-leader", 30*time.Second)
if errors.Is(err, contracts.ErrLockHeld) {
return nil // Another instance is running
}
defer handle.Unlock(ctx)
// ... critical section ...
}entries := fabric.Registry.FindByCapability("data.redaction")
if len(entries) > 0 {
rp := entries[0].Module.(contracts.DataRedactionProvider)
safe, _ := rp.Redact(ctx, data, []string{"email", "token"})
fabric.Audit.Log(ctx, contracts.AuditEntry{Action: "user.login", Data: safe})
}entries := fabric.Registry.FindByCapability("logging")
if len(entries) > 0 {
log := entries[0].Module.(contracts.StructuredLogger)
log.Info(ctx, "download started", map[string]any{
"task_id": task.ID,
"magnet": task.MagnetURI,
})
}entries := fabric.Registry.FindByCapability("config.watcher")
if len(entries) > 0 {
cw := entries[0].Module.(contracts.ConfigWatcher)
cancel, _ := cw.OnChange(ctx, "secrets", func() {
// Re-discover secrets provider
entries := fabric.Registry.FindByCapability("secrets")
if len(entries) > 0 {
secrets = entries[0].Module.(contracts.SecretsProvider)
}
})
defer cancel()
}entries := fabric.Registry.FindByCapability("deadletter")
fabric.EventBus.Subscribe(ctx, "workflow.step.completed", func(ctx context.Context, event contracts.Event) error {
if err := processEvent(event); err != nil {
if len(entries) > 0 {
dl := entries[0].Module.(contracts.DeadLetterProvider)
dl.Store(ctx, event, "my-handler", err)
}
return err
}
return nil
})entries := fabric.Registry.FindByCapability("identity")
if len(entries) > 0 {
idp := entries[0].Module.(contracts.IdentityProvider)
who, err := idp.ExtractIdentity(ctx)
if err != nil || who == nil {
return contracts.ErrUnauthorized
}
// who.ID, who.Roles, who.Claims available
}Use the SDK mock package to test without running a full platform:
import "github.com/Muxcore-Media/core/sdk/go/mock"
func TestMyModule(t *testing.T) {
deps := mock.NewDeps() // returns Fabric with mock Bus + Registry
mod := NewModule(deps.EventBus, deps.Registry)
// Register a mock module for discovery
mockMod := &MockDownloader{}
deps.Registry.(*mock.Registry).RegisterModule(mockMod)
err := mod.Init(context.Background())
assert.NoError(t, err)
}mock.NewDeps() gives you:
- An in-memory event bus (real implementation, safe for tests)
- A mock registry with
RegisterModule()for test fixtures - A no-op route registrar
Before publishing:
-
go.moddepends ongithub.com/Muxcore-Media/coreplus domain contract repos -
contracts.Register()called ininit() - 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
FindByCapability(), never assumed - Works gracefully when services are unavailable
-
Metaused for module-specific data instead of assuming core changes
- Contracts Reference — every interface in detail
- Module System — all module types and discovery
- Marketplace — publishing your module