SDK Installation

The TinySystems SDK provides everything you need to build custom modules and components.

Installation

The official example-module repository is a GitHub template with the correct layout, entrypoint, Dockerfile, and a working echo component. Click Use this template, clone your copy, and rename the Go module path.

Or Create a Module from Scratch

# Create project directory
mkdir my-module && cd my-module

# Initialize Go module (requires Go 1.25+)
go mod init github.com/myorg/my-module

# Install the SDK
go get github.com/tiny-systems/module@latest

Verify Installation

go list -m github.com/tiny-systems/module
# github.com/tiny-systems/module v0.13.x

SDK Package Overview

The SDK is organized into several packages:

PackageImport PathPurpose
modulegithub.com/tiny-systems/module/moduleCore interfaces (Component, Handler, Result, Port, lifecycle capabilities)
registrygithub.com/tiny-systems/module/registryComponent registration
cligithub.com/tiny-systems/module/cliCLI commands (run, tools, pre-install, pre-delete)
v1alpha1github.com/tiny-systems/module/api/v1alpha1CRD types and system port names (TinyNode, SettingsPort, ...)
evaluatorgithub.com/tiny-systems/module/pkg/evaluatorEdge expression evaluation
utilsgithub.com/tiny-systems/module/pkg/utilsHelpers (leader checks, port names)
errorsgithub.com/tiny-systems/module/pkg/errorsLow-level error utilities (prefer module.Retryable/module.Permanent)

Core Interfaces

Component Interface

The main interface you'll implement (from github.com/tiny-systems/module/module):

import "github.com/tiny-systems/module/module"

type Component interface {
    // Returns component metadata (name, description, info, tags)
    GetInfo() ComponentInfo

    // Processes a message arriving on `port`. Emit downstream by calling
    // `output`; return its Result (or module.Fail/module.Ok) so responses
    // can flow back to blocking callers.
    Handle(ctx context.Context, output Handler, port string, message any) Result

    // Defines input and output ports
    Ports() []Port

    // Factory method to create new instances
    Instance() Component
}

Handler Function

The callback for emitting to your own ports. It returns a Result — never discard it:

type Handler func(ctx context.Context, port string, data any) Result

Result carries either a payload or an error. Construct it with module.Ok(v) / module.Fail(err); inspect it with .Err(), .Value(), .IsErr(). Handler returns must propagate up the call chain — that is how blocking components (like http_server) receive the response that flows back from downstream nodes. Never call a handler with go output(...): that drops the Result.

Port Definition

Define component ports:

type Port struct {
    Name          string      // Unique lowercase identifier
    Label         string      // Display name in UI
    Position      Position    // module.Top, module.Right, module.Bottom, module.Left
    Source        bool        // true = OUTPUT port (a source of data)
    Configuration interface{} // Typed struct the schema is reflected from, e.g. InMessage{}
}

Inputs leave Source false. Configuration carries a typed struct instance (InMessage{} or new(OutMessage)); the framework reflects its JSON schema. The optional Schema field is only for runtime-authored schemas and stays nil in normal components.

Minimal Example

A module is a small Go program: components live in their own packages and self-register in init(); cmd/main.go blank-imports them and wires the SDK CLI into a cobra root command.

// components/echo/echo.go
package echo

import (
    "context"
    "fmt"

    "github.com/tiny-systems/module/module"
    "github.com/tiny-systems/module/registry"
)

const (
    ComponentName        = "echo"
    InPort        string = "in"
    OutPort       string = "out"
)

type Context any

type InMessage struct {
    Context Context `json:"context" configurable:"true" required:"true" title:"Context" description:"Arbitrary message to be echoed"`
}

type OutMessage struct {
    Context Context `json:"context" configurable:"true" title:"Context" description:"Passthrough — echoed unchanged"`
}

type Component struct{}

func (t *Component) Instance() module.Component {
    return &Component{}
}

func (t *Component) GetInfo() module.ComponentInfo {
    return module.ComponentInfo{
        Name:        ComponentName,
        Description: "Echo",
        Info:        "Sends the same message as it receives",
        Tags:        []string{"Echo", "Demo"},
    }
}

func (t *Component) Handle(ctx context.Context, handler module.Handler, port string, msg any) module.Result {
    if in, ok := msg.(InMessage); ok {
        return handler(ctx, OutPort, OutMessage{Context: in.Context})
    }
    return module.Fail(fmt.Errorf("invalid message"))
}

func (t *Component) Ports() []module.Port {
    return []module.Port{
        {Name: InPort, Label: "In", Configuration: InMessage{}, Position: module.Left},
        {Name: OutPort, Label: "Out", Source: true, Configuration: new(OutMessage), Position: module.Right},
    }
}

var _ module.Component = (*Component)(nil)

func init() {
    registry.Register(&Component{})
}
// cmd/main.go
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/rs/zerolog"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
    "github.com/tiny-systems/module/cli"

    // Import components to register them via init()
    _ "github.com/myorg/my-module/components/echo"
)

var rootCmd = &cobra.Command{
    Use:   "server",
    Short: "my-module",
    Run: func(cmd *cobra.Command, args []string) {
        cmd.Help()
    },
}

func main() {
    zerolog.SetGlobalLevel(zerolog.InfoLevel)
    viper.AutomaticEnv()
    if viper.GetBool("debug") {
        zerolog.SetGlobalLevel(zerolog.DebugLevel)
    }

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    cli.RegisterCommands(rootCmd)
    if err := rootCmd.ExecuteContext(ctx); err != nil {
        fmt.Printf("command execute error: %v\n", err)
    }
}

Running Your Module

Local Development

# Build the module
go build -o my-module ./cmd

# Run locally (connects to current kubectl context)
./my-module run \
    --name=my-module \
    --version=1.0.0 \
    --namespace=tinysystems

--name and --version are required. The gRPC server binds to :0 (random port) by default; the Helm chart sets it to :8483 in-cluster via --grpc-server-bind-address.

With Environment Variables

# Enable OpenTelemetry tracing
OTLP_DSN=http://localhost:4317 ./my-module run \
    --name=my-module \
    --version=1.0.0 \
    --namespace=tinysystems

Available CLI Commands

cli.RegisterCommands adds these commands to your binary:

# Run the module operator
./my-module run --name=my-module --version=1.0.0

# Print info about registered components (--json emits the
# discovery shape for a repo index's components.yaml)
./my-module tools components-info
./my-module tools components-info --json

# Emit the RBAC values block to commit with the module's index entry
./my-module tools rbac-values

# Report Kubernetes calls the declared RBAC does not cover
./my-module tools rbac-check [--strict]

# Helm lifecycle hooks (run by the chart, not by hand)
./my-module pre-install --name=my-module
./my-module pre-delete --name=my-module

There are no build, init, or generate commands — building is plain go build, and images are built with Docker.

See CLI Reference for details.

Dependencies

The SDK brings in these key dependencies:

require (
    // Kubernetes
    sigs.k8s.io/controller-runtime
    k8s.io/client-go
    k8s.io/apimachinery

    // gRPC (cross-module communication)
    google.golang.org/grpc

    // NATS JetStream (durable execution, optional)
    github.com/nats-io/nats.go

    // OpenTelemetry (observability)
    go.opentelemetry.io/otel

    // Expression evaluation
    github.com/tiny-systems/ajson
)

Next Steps