-
Notifications
You must be signed in to change notification settings - Fork 0
Writing Modules
This guide covers building a MuxCore module from scratch against the v1 fabric-only core.
A module is a Go package that:
- Implements
contracts.Module(lifecycle) - Calls
contracts.Register()ininit()(auto-discovery) - Implements one or more domain contract interfaces (from
contracts-*repos) - Has a
muxcore.jsonat its repo root (marketplace metadata)
my-module/
go.mod <- depends on core + contract repos
module.go <- your implementation
muxcore.json <- marketplace metadata
package mymodule
import (
"context"
"github.com/Muxcore-Media/core/pkg/contracts"
)
func init() {
contracts.Register(func(deps contracts.ModuleDeps) contracts.Module {
return NewModule(deps.EventBus, deps.Registry)
})
}
type Module struct {
bus contracts.EventBus
reg contracts.ServiceRegistry
}
func NewModule(bus contracts.EventBus, reg contracts.ServiceRegistry) *Module {
return &Module{bus: bus, reg: reg}
}
func (m *Module) Info() contracts.ModuleInfo {
return contracts.ModuleInfo{
ID: "my-module",
Name: "My Module",
Version: "1.0.0",
Kinds: []string{"provider"},
}
}
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 }Every module receives contracts.ModuleDeps with eight fabric services:
| Field | Interface | Use |
|---|---|---|
Registry |
ServiceRegistry |
Discover other modules at runtime |
EventBus |
EventBus |
Publish and subscribe to events |
Routes |
RouteRegistrar |
Register HTTP handlers |
Cluster |
Cluster |
Cluster membership (nil if standalone) |
Storage |
StorageOrchestrator |
Object storage via IDs, not paths |
WorkerPool |
WorkerPool |
Distributed task scheduling |
Audit |
AuditLogger |
System-wide audit logging |
Mesh |
ModuleMeshClient |
Cross-module gRPC calls |
Services NOT in ModuleDeps — discover at runtime via Registry.FindByCapability():
// Discover secrets provider
entries := deps.Registry.FindByCapability("secrets")
if len(entries) > 0 {
secrets = entries[0].Module.(contracts.SecretsProvider)
}
// Discover cache layer (for storage read-through caching)
entries = deps.Registry.FindByCapability("cache.local")
if len(entries) > 0 {
cache = entries[0].Module.(contracts.CacheLayer)
}Import github.com/Muxcore-Media/contracts-downloader. Implement dl.Downloader. Kind: "downloader".
import dl "github.com/Muxcore-Media/contracts-downloader"
func (m *Module) Add(ctx context.Context, task dl.DownloadTask) (string, error) { ... }
// task.Extra carries module-specific context (HTTP URLs, streaming params, etc.)Import github.com/Muxcore-Media/contracts-indexer. Implement indexer.Indexer. Kind: "indexer".
Import github.com/Muxcore-Media/contracts-playback. Implement playback.Playback and/or playback.StreamSource. Kind: "playback".
Import github.com/Muxcore-Media/contracts-media. Implement media.MediaLibrary. Kind: "media_manager".
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.Extra carries media-type-specific filters (artist, album, BPM, key, etc.)Implement contracts.StorageProvider (in core). Optionally implement Streamable, Seekable, Watchable, AtomicMovable, Hardlinkable. Kind: "storage".
Implement contracts.Scheduler (in core). Kind: "scheduler". SchedulerTask includes Extra map[string]any.
Implement contracts.AuthProvider (in core). Kind: "auth". Core wires the first auth module into API middleware automatically.
Register HTTP handlers via deps.Routes. Kinds: "ui", "api".
| Interface | Capability | Discovery |
|---|---|---|
SecretsProvider |
"secrets" |
FindByCapability("secrets") |
DatabaseProvider |
"database" |
FindByCapability("database") |
CacheProvider |
"cache" |
FindByCapability("cache") |
CacheLayer |
"cache.local" |
FindByCapability("cache.local") |
RateLimiterProvider |
— | Core picks up automatically |
HealthMonitor |
— | Core picks up automatically |
AuditLogger |
— | Injected via ModuleDeps.Audit
|
Cluster |
— | Injected via ModuleDeps.Cluster
|
TracingProvider |
"tracing" |
FindByCapability("tracing") |
CircuitBreaker |
"circuitbreaker" |
FindByCapability("circuitbreaker") |
ConfigWatcher |
"config.watcher" |
FindByCapability("config.watcher") |
DeadLetterProvider |
"deadletter" |
FindByCapability("deadletter") |
CallPolicyProvider |
"call.policy" |
FindByCapability("call.policy") |
IdentityProvider |
"identity" |
FindByCapability("identity") |
StructuredLogger |
"logging" |
FindByCapability("logging") |
RetryProvider |
"retry" |
FindByCapability("retry") |
IdempotencyProvider |
"idempotency" |
FindByCapability("idempotency") |
FeatureFlagProvider |
"feature.flags" |
FindByCapability("feature.flags") |
SerializationProvider |
"serialization" |
FindByCapability("serialization") |
EncryptionProvider |
"encryption" |
FindByCapability("encryption") |
DistributedLockProvider |
"distributed.lock" |
FindByCapability("distributed.lock") |
DataRedactionProvider |
"data.redaction" |
FindByCapability("data.redaction") |
EventStore |
"event.store" |
FindByCapability("event.store") |
Authorizer |
— | Core picks up automatically |
WorkerPool |
— | Injected via ModuleDeps.WorkerPool
|
// Broad: find all downloaders
entries := deps.Registry.FindByKind("downloader")
// Fine-grained: find by capability
entries := deps.Registry.FindByCapability("playback.jellyfin")
// Direct: resolve by ID
entry, err := deps.Registry.Resolve("downloader-qbittorrent")
// Type-assert to the domain contract
import dl "github.com/Muxcore-Media/contracts-downloader"
for _, entry := range entries {
if d, ok := entry.Module.(dl.Downloader); ok {
d.Add(ctx, task)
}
}Event types and payloads are defined in contract repos:
import dl "github.com/Muxcore-Media/contracts-downloader"
payload, _ := json.Marshal(dl.DownloadCompletedPayload{
DownloadID: id,
Title: title,
})
deps.EventBus.Publish(ctx, contracts.Event{
Type: dl.EventDownloadCompleted,
Source: "my-downloader",
Payload: payload,
})import dl "github.com/Muxcore-Media/contracts-downloader"
deps.EventBus.Subscribe(ctx, dl.EventDownloadCompleted,
func(ctx context.Context, event contracts.Event) error {
var payload dl.DownloadCompletedPayload
json.Unmarshal(event.Payload, &payload)
return nil
})result, err := deps.Mesh.Call(ctx, "transcoder-ffmpeg", "Status", jsonPayload)If a CallPolicyProvider is registered, the mesh client enforces access control. Set your module ID in the context before calling:
ctx = contracts.WithCallerID(ctx, "my-module")
result, err := deps.Mesh.Call(ctx, "target-module", "method", payload)Wrap cross-module calls in a circuit breaker to prevent cascading failures:
breaker := discoveredContracts.CircuitBreaker
err := breaker.Execute(ctx, "target-module", func(ctx context.Context) error {
_, err := deps.Mesh.Call(ctx, "target-module", "method", payload)
return err
})
if errors.Is(err, contracts.ErrCircuitOpen) {
// circuit is open, handle gracefully
}{
"name": "My Downloader",
"description": "A torrent downloader",
"version": "1.0.0",
"kind": "downloader",
"capabilities": ["downloader.torrent"],
"contracts": [
{"repo": "github.com/Muxcore-Media/contracts-downloader", "version": "v1.0.0", "interface": "Downloader"}
],
"expects": [
{"repo": "github.com/Muxcore-Media/contracts-secrets", "version": "v1.0.0", "interface": "SecretsProvider"}
],
"dependencies": [],
"homepage": "https://github.com/you/my-module"
}| Field | Required | Notes |
|---|---|---|
name |
Yes | Display name |
description |
Yes | Short description |
version |
Yes | Semantic version |
kind |
Yes | Primary kind string (e.g., "downloader") |
capabilities |
Yes | Array of capability strings |
contracts |
Yes | Contracts this module implements |
expects |
No | Contracts this module needs from others |
dependencies |
No | Module IDs this depends on |
homepage |
No | URL to repo |
- Push module to a public GitHub repo
- Add repo URL to a marketplace
catalog.json - For the official marketplace, submit a PR to
github.com/Muxcore-Media/marketplace-catalog
Create spans around operations for distributed tracing:
tracing, _ := deps.Registry.FindByCapability("tracing")
if len(tracing) > 0 {
tp := tracing[0].Module.(contracts.TracingProvider)
ctx, span := tp.StartSpan(ctx, "indexer.search")
defer span.End()
span.SetAttribute("query", query)
// ... do work ...
if err != nil {
span.SetStatus(contracts.SpanStatusError, err.Error())
}
}React to runtime changes without polling:
watcher, _ := deps.Registry.FindByCapability("config.watcher")
if len(watcher) > 0 {
cw := watcher[0].Module.(contracts.ConfigWatcher)
cancel, _ := cw.OnChange(ctx, "secrets", func() {
// Re-discover secrets provider
entries := deps.Registry.FindByCapability("secrets")
if len(entries) > 0 {
secrets = entries[0].Module.(contracts.SecretsProvider)
}
})
defer cancel()
}Handle critical event failures gracefully:
dl, _ := deps.Registry.FindByCapability("deadletter")
deps.EventBus.Subscribe(ctx, "workflow.step.completed", func(ctx context.Context, event contracts.Event) error {
if err := processEvent(event); err != nil {
if len(dl) > 0 {
dl[0].Module.(contracts.DeadLetterProvider).Store(ctx, event, "my-handler", err)
}
return err
}
return nil
})Extract caller identity for authorization checks:
identity, _ := deps.Registry.FindByCapability("identity")
if len(identity) > 0 {
idp := identity[0].Module.(contracts.IdentityProvider)
who, err := idp.ExtractIdentity(ctx)
if err != nil {
// invalid token, reject
return contracts.ErrUnauthorized
}
if who == nil {
// unauthenticated — apply anonymous policy
} else {
// pass to Authorizer
deps.Authorizer.Authorize(ctx, who.ID, "storage.write")
}
}Log with structured fields instead of raw prints:
logger, _ := deps.Registry.FindByCapability("logging")
if len(logger) > 0 {
log := logger[0].Module.(contracts.StructuredLogger)
log.Info(ctx, "download started", map[string]any{
"task_id": task.ID,
"magnet_uri": task.MagnetURI,
})
if err != nil {
log.Error(ctx, "download failed", map[string]any{
"task_id": task.ID,
"error": err.Error(),
})
}
}Retry transient failures with exponential backoff:
retry, _ := deps.Registry.FindByCapability("retry")
if len(retry) > 0 {
rp := retry[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 deps.Mesh.Call(ctx, "upstream", "Fetch", payload)
})
}
// If no RetryProvider registered, Execute runs fn once — safe either way.Prevent duplicate processing of the same request:
idem, _ := deps.Registry.FindByCapability("idempotency")
if len(idem) > 0 {
ip := idem[0].Module.(contracts.IdempotencyProvider)
isNew, err := ip.Store(ctx, requestID, cachedResult, 24*time.Hour)
if err != nil {
return err
}
if !isNew {
// Already processed — return cached result
cached, _ := ip.Result(ctx, requestID)
return cached, nil
}
}
// Process the request (first time only)...Gate new features behind flags:
flags, _ := deps.Registry.FindByCapability("feature.flags")
enabled := false
if len(flags) > 0 {
ff := flags[0].Module.(contracts.FeatureFlagProvider)
enabled = ff.IsEnabled(ctx, "new-recommendation-engine", false)
}
if enabled {
// run new code path
}
// For A/B testing:
variant := "control"
if len(flags) > 0 {
variant = flags[0].Module.(contracts.FeatureFlagProvider).GetVariant(ctx, "ui-layout", "control")
}Marshal/unmarshal without hardcoding format:
ser, _ := deps.Registry.FindByCapability("serialization")
if len(ser) > 0 {
sp := ser[0].Module.(contracts.SerializationProvider)
// Negotiate: pick first format both sides support
data, err := sp.Marshal("application/json", myStruct)
// Or let the provider handle format conversion:
var result MyType
sp.Unmarshal("application/x-protobuf", wireBytes, &result)
}Encrypt sensitive data before storage:
enc, _ := deps.Registry.FindByCapability("encryption")
if len(enc) > 0 {
ep := enc[0].Module.(contracts.EncryptionProvider)
ciphertext, err := ep.Encrypt(ctx, sensitiveData)
if err != nil {
return err
}
// Store ciphertext (self-describing, safe to decrypt later even after rotation)
deps.Storage.Put(ctx, "sensitive/key", ciphertext)
// Later:
stored, _ := deps.Storage.Get(ctx, "sensitive/key")
plaintext, err := ep.Decrypt(ctx, stored)
// Rotate keys on schedule:
ep.RotateKey(ctx)
}Coordinate across modules without relying on cache locks:
lock, _ := deps.Registry.FindByCapability("distributed.lock")
if len(lock) > 0 {
lp := lock[0].Module.(contracts.DistributedLockProvider)
handle, err := lp.Acquire(ctx, "migration-leader", 30*time.Second)
if errors.Is(err, contracts.ErrLockHeld) {
// Another instance is running the migration, skip
return nil
}
if err != nil {
return err
}
defer handle.Unlock(ctx)
// For long operations, extend the lease:
handle.Renew(ctx, 30*time.Second)
// ... critical section ...
}
// If no DistributedLockProvider, always succeeds — single-node mode.Redact sensitive fields before logging or audit:
redact, _ := deps.Registry.FindByCapability("data.redaction")
data := map[string]any{
"user_id": "abc123",
"email": "user@example.com",
"action": "login",
"token": "sk-abc123secret",
}
if len(redact) > 0 {
rp := redact[0].Module.(contracts.DataRedactionProvider)
safe, _ := rp.Redact(ctx, data, []string{"email", "token"})
// safe = {"user_id": "abc123", "email": "***REDACTED***", "action": "login", "token": "***REDACTED***"}
deps.Audit.Log(ctx, contracts.AuditEntry{
Action: "user.login",
Data: safe,
})
}
// If no DataRedactionProvider, data passes through unchanged — log safely.Append to and replay event streams for event sourcing:
store, _ := deps.Registry.FindByCapability("event.store")
if len(store) > 0 {
es := store[0].Module.(contracts.EventStore)
// Append events atomically:
seq, err := es.Append(ctx, "user-abc123", []contracts.Event{
{Type: "user.created", Source: "auth-module", Payload: userJSON},
{Type: "user.verified", Source: "auth-module", Payload: verifiedJSON},
})
// seq = 1 (first sequence number in the batch)
// Rebuild state by replaying from the beginning:
events, _ := es.Read(ctx, "user-abc123", 1, 0) // from seq 1, no limit
for _, entry := range events {
switch entry.Event.Type {
case "user.created":
applyUserCreated(entry.Event.Payload)
case "user.verified":
applyUserVerified(entry.Event.Payload)
}
}
// Subscribe to live events from the last seen sequence:
ch, _ := es.Subscribe(ctx, "user-abc123", events[len(events)-1].Sequence+1)
for entry := range ch {
handleEvent(entry)
}
}-
go.moddepends ongithub.com/Muxcore-Media/coreplus contract repos you implement -
contracts.Register()called ininit() - Implements
contracts.Module(Info, Init, Start, Stop, Health) - Implements the domain contract from the appropriate
contracts-*repo -
ContractDeclarationsinInfo().Contractsfor compatibility discovery -
muxcore.jsonat repo root withcontractsandexpectsfields - Module discovers services it needs via
Registry.FindByCapability()