Create Your Own Module
The fastest way to start building your own TinySystems module is to use the example-module template repository. It comes pre-configured with a working component, GitHub Actions CI/CD, and a release script.
Prerequisites
- Go 1.25+
- A GitHub account (or any Git hosting with Actions support)
- A container registry the workflow can push to (the template uses GHCR)
Step 1: Create a Repository from the Template
Go to github.com/tiny-systems/example-module and click Use this template to create a new repository in your GitHub organization or personal account.
my-org/my-awesome-module
Clone your new repository locally:
git clone https://github.com/my-org/my-awesome-module
cd my-awesome-module
Update go.mod to match your repository path:
go mod edit -module github.com/my-org/my-awesome-module
No account, developer key, or secret setup is needed — the workflow pushes to GHCR with the repository's built-in GITHUB_TOKEN.
Step 2: Write Your Component
The template includes an echo component at components/echo/echo.go. Use it as a starting point or replace it with your own.
Here's the echo component for reference:
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"`
}
// OutMessage keeps the passthrough Context under a `context` key so a
// downstream edge reads $.context.<field> — the mid-chain convention.
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 interface{}) 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{})
}
Note the shape of Handle: it returns module.Result, and the handler(...) return is passed straight through — that chaining is what lets synchronous responses flow back to blocking callers upstream.
To add more components, create a new package under components/ and register it with a blank import in cmd/main.go:
import (
_ "github.com/my-org/my-awesome-module/components/echo"
_ "github.com/my-org/my-awesome-module/components/mycomponent"
)
Step 3: Publish with a Git Tag
The template includes a GitHub Actions workflow (.github/workflows/release.yml) that automatically builds and publishes your module when you push a semver tag:
git tag v0.0.1
git push origin v0.0.1
A convenience script is also included:
./release.sh # interactive - asks for patch/minor/major
./release.sh patch # direct bump
What Happens on Tag Push
- GitHub Actions checks out your code
- Builds a container image from your Dockerfile, for amd64 and arm64
- Pushes it to GHCR, tagged with the version without the leading
v(v0.0.1→ghcr.io/my-org/my-awesome-module:0.0.1) — that bare tag is what the module index references
There is no publish step and no developer key. The image is the whole artifact.
The workflow file (.github/workflows/release.yml):
name: Publish module image to GHCR
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Image tag is the version without the leading 'v' (v0.5.24 -> 0.5.24),
# matching what the module index references.
- name: Derive version
id: v
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ steps.v.outputs.version }}
ghcr.io/${{ github.repository }}:latest
build-args: |
VERSION=${{ steps.v.outputs.version }}
Note that GHCR packages default to private on first push. Make yours public once, or no cluster can pull it.
Step 4: List It in a Module Repo
Modules are discovered from repo indexes, not pushed anywhere. In a module repo (a directory of manifests hosted as static files — see tiny-systems/modules for the public one), add a directory for your module:
my-module/module.yaml:
name: my-module
source: github.com/my-org/my-module
description: What it does
category: core
versions:
- version: 1.0.0
image: ghcr.io/my-org/my-module:1.0.0
chart: tinysystems/tinysystems-operator
chartVersion: 0.2.10
Generate components.yaml beside it from the built image, so consumers can browse your components before installing:
docker run --rm --entrypoint /manager ghcr.io/my-org/my-module:1.0.0 \
tools components-info --json > my-module/components.yaml
If your module calls the Kubernetes API, it must declare what it needs.
Generate the values.yaml overlay from the image rather than writing it by hand —
the install grants exactly what is declared, so a missing verb becomes a 403 at
runtime:
docker run --rm --entrypoint /manager ghcr.io/my-org/my-module:1.0.0 \
tools rbac-values > my-module/values.yaml
tools rbac-check, run in your module's source directory, reports any Kubernetes
call your code makes that the declaration does not cover.
Then regenerate the index and publish the repo (GitHub Pages works):
tiny repo index . -o index.yaml
Step 5: Install Your Module
Consumers learn about your module by adding your repo's index:
tiny repo add myrepo https://my-org.github.io/modules/index.yaml
tiny repo update
tiny install my-module
On the platform, a workspace registers module repos the same way — once your index is in a workspace's repo list, your module appears in its catalog and can be installed into any connected cluster. Your components then show up in the visual editor's component palette, ready to use in flows.
Updating the SDK
To update to the latest SDK version:
./release.sh update
# or manually:
go get github.com/tiny-systems/module@latest
go mod tidy
Project Structure
my-awesome-module/
├── cmd/
│ └── main.go # Entry point, cli.RegisterCommands + blank imports
├── components/
│ └── echo/
│ └── echo.go # Component implementation
├── .github/
│ └── workflows/
│ └── release.yml # CI/CD pipeline
├── Dockerfile # golang:1.25 -> distroless static:nonroot
├── release.sh # Release helper script
├── go.mod
└── README.md
Next Steps
- Component Interface — full interface reference
- Defining Ports — port configuration and struct tags
- Hello World Component — detailed component tutorial
- System Ports —
_settings,_control,_reconcile