Moul

Custom Binary Embedding (pkg/app)

Embed the complete Moul engine in your Go binary with embedded Web Admin Console, custom HTTP routes, and background worker handlers.

pkg/app allows embedding the complete moul server into custom Go binaries with tailored HTTP endpoints, background workers, and the pre-built Web Admin Console out-of-the-box.


Overview

When embedding Moul as a Go library:

  • Built-in Web Admin Console: The production Web Admin Console (TanStack Router, Meta StyleX, React Aria) is embedded directly into your binary via pkg/ui.DistFS() and served at /_moul_/.
  • Zero Node.js / Bun Requirement: Compiled static assets are bundled inside github.com/moul-dev/moul-dev/pkg/ui/dist, allowing custom binary compilation using standard go build.
  • Safe Route Prefixing: By default, the admin console mounts strictly at AdminUIPrefix (/_moul_/) without intercepting /admin, preventing route collision with your host application.
  • Full Customizability: Override the console with your own frontend SPA, mount on a custom URL prefix, or disable the console entirely for headless API microservices.

Quickstart Example

Here is a complete, runnable custom binary example embedding Moul with custom routes, a background worker, and the embedded Web Admin Console:

package main

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/labstack/echo/v5"
	"github.com/moul-dev/moul-dev/pkg/app"
	"github.com/moul-dev/moul-dev/pkg/worker"
)

func main() {
	// 1. Initialize Moul application instance
	// The Web Admin Console is automatically bundled and served at http://localhost:8090/_moul_/
	moulApp := app.New(app.Config{
		Version: "1.0.0-custom",
	})

	// 2. Register custom HTTP route
	moulApp.RegisterRoute("GET", "/api/custom/ping", func(c *echo.Context) error {
		return c.JSON(http.StatusOK, map[string]string{
			"status": "pong",
			"server": "custom-moul",
		})
	})

	// 3. Attach directly to raw Echo router
	moulApp.OnRouterInit(func(router *echo.Echo) error {
		v1 := router.Group("/api/v1")
		v1.GET("/health", func(c *echo.Context) error {
			return c.String(http.StatusOK, "Engine healthy")
		})
		return nil
	})

	// 4. Register custom background job worker
	moulApp.RegisterWorker("ProcessThumbnail", func(ctx context.Context, job *worker.Job) error {
		imageURL, ok := job.Args["image_url"].(string)
		if !ok {
			return fmt.Errorf("missing image_url")
		}
		slog.Info("Processing thumbnail", "url", imageURL)
		return nil
	})

	// 5. Register periodic cron worker
	moulApp.RegisterPeriodicWorker(24*time.Hour, "DailyReport", func(ctx context.Context, job *worker.Job) error {
		slog.Info("Running daily report cleanup...")
		return nil
	})

	// 6. Start the server (supports CLI subcommands such as "mcp", "worker", "seed", "start")
	if !app.IsMCP() {
		fmt.Println("🚀 Custom Moul server running at http://localhost:8090")
		fmt.Println("🛠️  Web Admin Console at       http://localhost:8090/_moul_/")
	}
	if err := moulApp.Start(context.Background()); err != nil {
		slog.Error("Server failed", "err", err)
		os.Exit(1)
	}
}

Configuring the Web Admin Console

Mount the Web Admin Console on a different path, such as /dashboard:

// Via Config:
moulApp := app.New(app.Config{
    AdminUIPrefix: "/dashboard",
})

// Or via fluent builder:
moulApp.WithAdminPrefix("/dashboard")

Supply your own embedded SPA (fs.FS) instead of the default Moul admin console:

//go:embed all:frontend/dist
var myCustomFrontend embed.FS

// Strip dist prefix to root filesystem
subFS, _ := fs.Sub(myCustomFrontend, "frontend/dist")

// Attach custom SPA filesystem:
moulApp.WithAdminUI(subFS).WithAdminPrefix("/app")

All static assets and client-side HTML5 SPA sub-routes will automatically be resolved and served with caching and SPA index fallback.

Disable mounting the Web Admin Console completely for lightweight or API-only microservices:

// Via Config:
moulApp := app.New(app.Config{
    DisableAdminUI: true,
})

// Or via fluent builder:
moulApp.DisableAdminUI()

Customize the URL path prefix for all API routes (or mount at root using ""):

// Mount at /v1 instead of default /api
moulApp.WithAPIPrefix("/v1")

// Or mount directly at root level
moulApp.WithAPIPrefix("")

Configuration Reference (app.Config)

FieldTypeDefaultDescription
Versionstring"dev"Application release version string.
Envstring"development"Runtime environment ("development" or "production").
DBPathstring"moul-local.db"SQLite database file path or ":memory:".
Portstring"8090"HTTP listener port.
APIPrefix*stringnil ("/api")URL path prefix for API endpoints. Set to pointer to "" or use WithAPIPrefix("") to mount at root.
JWTSecretstringMOUL_JWT_SECRETSecret key for signing and verifying JWT tokens.
AdminKeystringMOUL_ADMIN_KEYMaster administrative API key.
AdminUIFSfs.FSpkg/ui.DistFS()Filesystem containing web console assets. Defaults to embedded Moul UI bundle.
AdminUIPrefixstring"/_moul_"URL path prefix where the Web Admin Console is mounted.
DisableAdminUIboolfalseWhen true, completely disables registering the Web Admin Console routes.
DisableCLIParsingboolfalseWhen true, disables automatic CLI argument dispatching so Start() always boots the HTTP server.

Model Context Protocol (MCP) in Custom Binaries

Custom binaries built with pkg/app natively support the built-in MCP server over stdio (my-binary mcp) and Streamable HTTP / SSE (/api/mcp).

Stdio Transport Mode (my-binary mcp)

Configure your custom binary directly in your AI assistant's MCP configuration (Claude Desktop, Cursor, Gemini, Antigravity):

{
  "mcpServers": {
    "my-app": {
      "command": "/path/to/my-binary",
      "args": ["mcp"],
      "env": {
        "MOUL_DB_PATH": "./moul-local.db"
      }
    }
  }
}

Key features of stdio mode in embedded custom binaries:

  • Clean Stdio Channel: Stdio mode reserves stdout exclusively for JSON-RPC messages. Always guard terminal banners or raw fmt.Println output with if !app.IsMCP() { ... } or write diagnostic logs to stderr using logger.Info / slog.Info.
  • Integrated Worker Queues: Custom background workers registered with moulApp.RegisterWorker are immediately accessible to MCP tools (such as moul_list_worker_jobs and moul_enqueue_job).
  • No Secrets Required: Stdio mode runs locally with the user's execution privileges and operates directly on SQLite without requiring MOUL_JWT_SECRET or MOUL_ADMIN_KEY.
  • Programmatic Control: Use moulApp.RegisterMCPTool(tool, handler) to add custom domain tools to the AI assistant, or hook into moulApp.OnMCPInit(...).
  • Dual HTTP Authentication: In HTTP mode (my-binary start), remote MCP clients can authenticate using either Authorization: Bearer <ADMIN_KEY> or X-Admin-Key: <ADMIN_KEY>.

Lifecycle Hooks Reference

RegisterRoute(method, path, handler, middleware...)

Registers a single HTTP handler directly on the embedded router before server start.

RegisterMCPTool(tool, handler)

Registers a custom Model Context Protocol (MCP) tool that is automatically exposed across both Stdio and Streamable HTTP / SSE transports.

OnMCPInit(callback)

Hook that executes when the built-in MCP server is initialized, allowing low-level access to the underlying MCP server.

OnRouterInit(callback)

Provides access to the underlying *echo.Echo instance immediately after core Moul routes are configured. Enables custom middleware registration, route grouping (router.Group(...)), and specialized handler mounting.

OnBeforeStart(callback)

Fires after SQLite database connections and dynamic collections are initialized, but before HTTP listening begins. Ideal for running custom migrations or seeding initial collection fixtures via app.DB().

OnWorkerInit(callback)

Executes when the worker engine initializes, providing access to *worker.Engine for advanced queue registration.


Standalone Examples

Complete, standalone runnable examples are available in the repository:

On this page