sdk

package module
v0.0.0-...-2dd0e01 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

README

tabibu-ext-sdk

Go SDK for building Tabibu extensions. Import it in your extension's main package, implement the Extension interface, and call sdk.Run or create a new extension with:

tabibu extension init my-extension

Extensions run as native child processes managed by the Tabibu server — a similar model VS Code uses for its extensions. The SDK handles all IPC and lifecycle details.


How it works

Tabibu Server
│
│  stdin  ──────────────────────────→  Extension process
│  (events, actions, config, shutdown)
│
│  stdout ←──────────────────────────  Extension process
│  (service requests, heartbeats, drain_done)
│
│  HTTP   ←──────────────────────────  Extension WebView
│  (EXT_HTTP_PORT, Pine server inside the extension)

Each message is a single JSON line (NDJSON). The SDK reads from os.Stdin and writes to os.Stdout automatically — extension authors write zero protocol code.


Installation

go get github.com/Nexus-Labs-254/tabibu-ext-sdk

Quick start

package main

import (
    "context"
    "log"

    sdk "github.com/Nexus-Labs-254/tabibu-ext-sdk"
)

func main() {
    if err := sdk.Run(&MyExtension{}); err != nil {
        log.Fatal(err)
    }
}

type MyExtension struct{}

func (e *MyExtension) OnStart(ctx context.Context, server sdk.Server) error {
    server.Get("/hello", func(c sdk.Ctx) error {
        return c.JSON(map[string]string{"hello": "world"})
    })
    return nil
}

func (e *MyExtension) OnEvent(ctx context.Context, event sdk.Event) error {
    sdk.Log.Info("event received", map[string]any{"name": event.Name})
    return nil
}

func (e *MyExtension) OnShutdown(ctx context.Context) error {
    return nil // finish in-flight work here
}

func (e *MyExtension) OnConfigUpdate(ctx context.Context, cfg sdk.Config) error {
    // cfg["EXAMPLE_KEY"] — read-only values set in the Tabibu admin panel
    return nil
}

Env vars

These are set by the Extension Runtime when it spawns your process. You do not set them yourself in production; in dev mode, put them in .env:

Variable Description
EXT_NAME Extension name (matches manifest.toml)
EXT_HTTP_PORT Port for the Pine HTTP server (WebView and extension routes)
EXT_DATA_DIR Persistent data directory for this extension
EXT_DEV "true" in dev mode — disables static UI serving
EXT_SERVER_URL Tabibu server URL — used only by sdk.HTTPClient()

Service layer

Instead of calling Tabibu's HTTP APIs directly, use the service accessors. Calls are routed through the IPC channel to the Extension Runtime, which executes them in-process.

sdk.Patients()
// List patients
patients, err := sdk.Patients().List(ctx, "John")

// Get a single patient
patient, err := sdk.Patients().Get(ctx, "uuid-here")

// Register a new patient
patient, err := sdk.Patients().Register(ctx, sdk.RegisterPatientRequest{
    GivenName:  "Jane",
    FamilyName: "Doe",
    Sex:        "F",
    Phone:      "+254700000000",
})
sdk.GetConfig()

Returns the extension's own config map — key/value pairs declared in manifest.toml [extension.config] and editable in the Tabibu admin panel. Values are read-only from the extension's perspective; updates flow inward via OnConfigUpdate.

cfg := sdk.GetConfig()
shortcode := cfg["shortcode"]
sdk.HTTPClient()

Returns a *client that is pre-authenticated with a JWT (exchanged from the API key written to $EXT_DATA_DIR/.api_key). Use this for Tabibu API calls not yet covered by the service layer.

resp, err := sdk.HTTPClient().Get(ctx, "/v1/billing/bills/"+billID)

Events

Subscribe to events by declaring them in manifest.toml:

[[contributes.events]]
subscribe = "billing.payment_requested"

The Extension Runtime subscribes to the broker on your behalf and delivers matching events to OnEvent over stdin. Extensions never hold broker credentials.

func (e *MyExtension) OnEvent(ctx context.Context, event sdk.Event) error {
    switch event.Name {
    case sdk.EventPaymentRequested:
        var payload sdk.PaymentRequestedPayload
        _ = json.Unmarshal(event.Payload, &payload)
        // handle payment...
    }
    return nil
}

Dev mode

Go backend

In dev mode the Extension Runtime spawns your extension using go run . in the source directory instead of the compiled binary. This lets tools like air manage restarts on file changes.

No extra configuration is needed. When you run:

tabibu server start --dev --ext-dev-dir ./path/to/my-extension

the server:

  1. Reads manifest.toml from the dev path
  2. Registers the extension in the DB (idempotent)
  3. Spawns go run . in the source directory with EXT_DEV=true

Reload after a Go change:

tabibu extension reload my-extension

Or install air for automatic restarts:

go install github.com/air-verse/air@latest
# In your extension directory:
air
UI hot reload (Vite)

Declare dev_port in manifest.toml:

[extension.ui]
has_ui   = true
dev_port = 5173

When EXT_DEV=true, the Extension Runtime proxies GET /v1/ui/<name>/* to http://localhost:<dev_port> instead of the compiled ui/dist/. Start Vite separately:

cd my-extension/ui && npm run dev   # Vite listens on dev_port

Changes appear instantly — no reload needed.

In production (no EXT_DEV), the Runtime proxies to the Pine HTTP server inside the extension process (EXT_HTTP_PORT), which serves ui/dist/ as a SPA.


manifest.toml reference

[extension]
name        = "my-extension"
version     = "1.0.0"
description = "Does something useful"
author      = "You <you@example.com>"
category    = "billing"
min_tabibu  = "1.0.0"

[extension.privileges]
required = "billing.view"    # leave empty to allow any authenticated user

[extension.ui]
has_ui   = false
dev_port = 5173              # Vite port — only used when EXT_DEV=true

[runtime]
binary            = "bin/my-extension"  # relative path inside the .tabibu package
stop_grace_period = 30                  # seconds before SIGKILL after shutdown

[[contributes.actions]]
id      = "billing.pay_mpesa"
label   = "Pay via M-Pesa"
context = "billing.bill"

[[contributes.events]]
subscribe = "billing.payment_requested"

[extension.config]
shortcode    = ""   # editable in Tabibu admin panel; read via sdk.GetConfig()
callback_url = ""

Production packaging

# 1. Build and package
tabibu extension build .
# → my-extension-1.0.0.tabibu

# 2. Install on server
tabibu extension install ./my-extension-1.0.0.tabibu

# 3. Or install from configured registry
tabibu extension install my-extension

The .tabibu archive contains:

my-extension-1.0.0.tabibu
    manifest.toml
    bin/
        my-extension-linux-amd64
        my-extension-darwin-arm64
    ui/dist/          (optional, if has_ui = true)
    signature.sig     (optional, Ed25519 over SHA256 of manifest.toml)

To sign packages, set TABIBU_SIGN_KEY to a base64-encoded Ed25519 private key before running tabibu extension build. The server verifies the signature when extensions.registry_public_key is configured.


Graceful shutdown

The Extension Runtime sends {"type":"shutdown","grace_seconds":N} on stdin. The SDK:

  1. Calls OnShutdown (30 s timeout)
  2. Writes {"type":"drain_done"} to stdout
  3. Cancels the context and exits 0

If the process doesn't exit within stop_grace_period seconds, the Runtime sends SIGTERM then SIGKILL. The WatchSignal goroutine inside the SDK handles SIGTERM as a fallback (same drain sequence, same drain_done message).

Documentation

Overview

Package sdk is the Tabibu Extension SDK. Import it in your extension's main package and call Run with your Extension implementation.

The SDK handles:

  • .env file loading on startup (dev convenience)
  • stdio JSON-RPC (NDJSON) over inherited stdin/stdout
  • Pine HTTP server on EXT_HTTP_PORT (default 9000)
  • Static UI serving from ui/dist/ in production (EXT_DEV != "true")
  • Domain service calls via sdk.Patients() and friends
  • Graceful drain on shutdown message or SIGTERM → drain_done → exit 0

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func HTTPClient

func HTTPClient() *client

HTTPClient returns a pre-authenticated *http.Client for making direct calls to the Tabibu server. Use this only when sdk.Patients() (or other service accessors) don't cover what you need.

func Run

func Run(ext Extension) error

Run starts the extension. It blocks until the process is told to shut down.

  1. Loads .env (dev convenience; existing vars take precedence)
  2. Reads env vars: EXT_NAME, EXT_HTTP_PORT, EXT_DATA_DIR, EXT_DEV, EXT_SERVER_URL
  3. Opens the log file in EXT_DATA_DIR/logs/extension.log
  4. Initialises the stdio IPC conn (stdin/stdout are inherited from parent)
  5. Reads the API key from EXT_DATA_DIR/.api_key for sdk.HTTPClient()
  6. Calls ext.OnStart(ctx, server)
  7. In prod: serves ui/dist/ as a SPA if the directory exists
  8. Starts dispatching stdin messages (events, shutdown, config_update)
  9. SIGTERM fallback: triggers drain if runtime message never arrives

Types

type Config

type Config map[string]string

Config is a read-only map of key-value pairs from the extension's config section in manifest.toml, overridden by admin in the Tabibu panel. Updated automatically when an OnConfigUpdate message arrives on stdin.

func GetConfig

func GetConfig() Config

Config returns the current extension config map (read-only).

type Ctx

type Ctx interface {
	Status(code int) Ctx
	JSON(v any) error
	BindJSON(v any) error
	Params(key string) string
	Query(key string) string
	Context() context.Context
	Header(key string) string
}

Ctx is the request context passed to handler functions.

type Event

type Event struct {
	Name       string          `json:"name"`
	Version    string          `json:"version"`
	OccurredAt time.Time       `json:"occurred_at"`
	Payload    json.RawMessage `json:"payload"`
	ID         string          `json:"id"`
}

Event carries a single event dispatched to the extension.

type Extension

type Extension interface {
	// OnStart is called once after the SDK is ready. Register HTTP routes
	// on the provided Server. Return a non-nil error to abort startup.
	OnStart(ctx context.Context, server Server) error

	// OnEvent is called for each event routed to this extension by the
	// Extension Runtime (declared in manifest.toml contributes.events).
	// Return a non-nil error to signal processing failure.
	OnEvent(ctx context.Context, event Event) error

	// OnShutdown is called when Tabibu sends a drain signal (shutdown message
	// on stdin or SIGTERM). Finish in-flight work and return. The SDK then
	// writes drain_done to stdout and exits 0. The process is force-killed
	// after stop_grace_period seconds.
	OnShutdown(ctx context.Context) error

	// OnConfigUpdate is called when the extension's config is changed in the
	// Tabibu admin panel. Values are pushed via the stdio config_update message.
	// Use sdk.Config() to read the current config map at any time.
	OnConfigUpdate(ctx context.Context, cfg Config) error
}

Extension is the interface every Tabibu extension must implement.

type HandlerFunc

type HandlerFunc func(Ctx) error

HandlerFunc is the signature for HTTP handlers registered via Server.

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger writes structured JSON log entries to both logs/extension.log and stderr. The log file is opened with O_APPEND so existing content is preserved across restarts. In containerised deployments log shipping (e.g. Fluent Bit) reads from the file; rotation is handled by the container runtime or logrotate.

var Log *Logger

Log is the SDK's structured logger. Available after Run() has started.

func (*Logger) Error

func (lg *Logger) Error(msg string, fields ...map[string]any)

func (*Logger) Info

func (lg *Logger) Info(msg string, fields ...map[string]any)

func (*Logger) Warn

func (lg *Logger) Warn(msg string, fields ...map[string]any)

type Patient

type Patient struct {
	ID            string        `json:"id"`
	BloodGroup    *string       `json:"blood_group,omitempty"`
	AllergyStatus string        `json:"allergy_status"`
	CreatedAt     string        `json:"created_at"`
	Person        PatientPerson `json:"person"`
}

Patient is a patient record as returned by the Extension Runtime. Fields mirror the server's patients.models.Patient JSON output.

type PatientPerson

type PatientPerson struct {
	ID                 string  `json:"id"`
	GivenName          string  `json:"given_name,omitempty"`
	MiddleName         string  `json:"middle_name,omitempty"`
	FamilyName         string  `json:"family_name,omitempty"`
	Salutation         string  `json:"salutation,omitempty"`
	Sex                string  `json:"sex"`
	Birthdate          *string `json:"birthdate,omitempty"`
	BirthdateEstimated bool    `json:"birthdate_estimated"`
	PrimaryPhone       *string `json:"primary_phone,omitempty"`
	AltPhone           *string `json:"alt_phone,omitempty"`
	Email              *string `json:"email,omitempty"`
	PhotoURL           *string `json:"photo_url,omitempty"`
}

PatientPerson holds the demographic and contact data for a patient. Fields mirror the server's persons.models.Person JSON output.

type PatientsService

type PatientsService interface {
	// List returns patients whose name or ID matches query. Pass an empty
	// string to return all patients.
	List(ctx context.Context, query string) ([]Patient, error)

	// Get returns a single patient by UUID.
	Get(ctx context.Context, id string) (Patient, error)

	// Register creates a new patient record.
	Register(ctx context.Context, req RegisterPatientRequest) (Patient, error)
}

PatientsService provides read and write access to the patients domain. Calls are routed through the stdio IPC channel to the Extension Runtime, which forwards them to the server's patients module.

func Patients

func Patients() PatientsService

Patients returns the patients domain service backed by the IPC channel. Call from within handler functions or background goroutines after Run() starts.

type RegisterPatientRequest

type RegisterPatientRequest struct {
	GivenName          string  `json:"given_name"`
	MiddleName         string  `json:"middle_name,omitempty"`
	FamilyName         string  `json:"family_name"`
	Salutation         string  `json:"salutation,omitempty"`
	Sex                string  `json:"sex"`
	Birthdate          string  `json:"birthdate,omitempty"`
	BirthdateEstimated bool    `json:"birthdate_estimated,omitempty"`
	BloodGroup         *string `json:"blood_group,omitempty"`
	AllergyStatus      string  `json:"allergy_status,omitempty"`
	Phone              string  `json:"primary_phone,omitempty"`
	AltPhone           string  `json:"alt_phone,omitempty"`
	Email              string  `json:"email,omitempty"`
}

RegisterPatientRequest is the payload for registering a new patient. Fields mirror the server's patients.models.RegisterRequest.

type Server

type Server interface {
	Get(path string, h HandlerFunc)
	Post(path string, h HandlerFunc)
	Put(path string, h HandlerFunc)
	Delete(path string, h HandlerFunc)
}

Server is the subset of Pine's API exposed to OnStart.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL