Skip to content

Writing Modules

TheMinecraftGuyGuru edited this page Jun 8, 2026 · 5 revisions

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.


Before You Start

You need:


The Simplest Module

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

Step 1: Create the Go Module

mkdir my-module && cd my-module
go mod init github.com/yourname/my-module
go get github.com/Muxcore-Media/core

Step 2: Implement the Module

package 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 }

Step 3: Add Marketplace Metadata

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.


What the Platform Gives You

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
    }
}

Discovering Other Modules

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"},
        })
    }
}

Communicating Through Events

Publishing an Event

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,
})

Subscribing to Events

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
    })

Calling Other Modules Directly

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)

Receiving Direct Calls

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)
    }
}

Building a Real Module

Here's how to build modules for common roles. Each requires importing the appropriate domain contract repo.

Downloader Module

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"},
        },
    }
}

Media Manager Module

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"},
        },
    }
}

Storage Provider Module

// 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"},
    }
}

Using Infrastructure Services

Secrets

entries := fabric.Registry.FindByCapability("secrets")
if len(entries) > 0 {
    secrets := entries[0].Module.(contracts.SecretsProvider)
    apiKey, _ := secrets.Get(ctx, "api_key")
}

Tracing

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")
}

Circuit Breaker

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
    }
}

Retry

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)
    })
}

Idempotency

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
    }
}

Feature Flags

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)
}

Encryption

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)
}

Distributed Lock

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 ...
}

Data Redaction

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})
}

Structured Logging

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,
    })
}

Config Watcher

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()
}

Dead Letter

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
})

Identity

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
}

Testing Your Module

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

Module Checklist

Before publishing:

  • go.mod depends on github.com/Muxcore-Media/core plus domain contract repos
  • contracts.Register() called in init()
  • Implements contracts.Module (Info, Init, Start, Stop, Health)
  • Implements domain contracts from appropriate contracts-* repos
  • Contracts declared in Info().Contracts
  • muxcore.json at repo root with contracts and capabilities
  • All external services discovered via FindByCapability(), never assumed
  • Works gracefully when services are unavailable
  • Meta used for module-specific data instead of assuming core changes

Next Steps

Clone this wiki locally