Skip to content

How Salieno Core's Plugin Architecture Actually Works Under the Hood

A technical deep dive into Salieno Core's plugin system: the hook registry, lifecycle management, and the design trade-offs we made to keep it simple and fast.

Written by AISali·August 4, 2026·5 min read
How Salieno Core's Plugin Architecture Actually Works Under the Hood

Why Plugins Instead of Configuration#

When we first sketched out Salieno Core, the biggest question wasn't what features to build — it was how to let hosting operators extend the platform without forking the codebase or waiting on us for every niche integration.

Configuration flags and environment variables get you part of the way. But the moment someone needs to hook into account provisioning, transform a billing invoice, or add a custom panel page, static config falls apart. You end up with a settings file that's 4,000 lines long and still can't do what a customer needs.

So we committed to a plugin architecture early. Not a plugin marketplace — we're not WordPress — but a first-class system where external code can register with the core binary, react to lifecycle events, and extend the control panel UI. Here's how it actually works.

The Hook Registry: A Central Event Bus#

At the heart of Salieno Core's plugin system is a synchronous, in-process hook registry. Every significant action in the platform — creating an account, suspending a site, generating an invoice, processing a DNS zone update — emits a named event through this registry.

Plugins register interest in specific hooks during initialization. When an event fires, the registry calls each registered handler in the order they were loaded. Handlers receive a context struct containing the relevant data (account ID, domain, billing cycle, etc.) and can modify it, short-circuit the action, or perform side effects.

go
// Simplified example of how a plugin registers a hook
func (p *MyPlugin) Init(core salieno.CoreAPI) error {
    core.Hooks().On("account.created", p.sendWelcomeEmail)
    core.Hooks().On("invoice.generated", p.applyCustomTax)
    return nil
}

The key design choice here is synchronous execution. We considered async hooks but decided the complexity wasn't worth it for the hosting use case. When you create an account, you want the DNS zone, the billing record, and the provisioning to complete in a predictable order — not scatter across goroutines with unclear failure modes. If a plugin needs to do something slow (like calling an external API), it can spawn its own goroutine inside the handler.

Plugin Lifecycle: Load, Init, Stop#

Each plugin is a Go shared object (.so file) compiled against the Salieno Core SDK. On startup, the core binary scans a configured plugin directory, loads each .so via plugin.Open, and looks for an exported Plugin variable that satisfies the salieno.Plugin interface.

The lifecycle has three phases:

  • Load: The shared object is loaded into memory. No code runs yet beyond Go's init functions.
  • Init: The core calls Plugin.Init(), passing a CoreAPI handle. This is where plugins register hooks, declare custom routes, and perform any setup. If Init returns an error, the plugin is unloaded and the error is logged — but the server continues starting.
  • Stop: On graceful shutdown, each plugin's Stop() method is called in reverse load order, giving plugins a chance to flush state, close connections, or notify external services.

This simplicity is deliberate. We looked at plugin systems like HashiCorp's go-plugin (which uses gRPC over stdio) and decided it was over-engineered for a single-binary platform. Shared objects have drawbacks — a crashing plugin can take down the host process — but they're fast, simple, and well-understood.

The CoreAPI Surface#

Plugins don't get unrestricted access to Salieno Core's internals. They interact through a CoreAPI interface that exposes a curated set of operations:

  • Account management: Create, suspend, unsuspend, and delete hosting accounts.
  • Billing: Read and modify invoices, apply credits, and query payment status.
  • DNS: Add, modify, and remove DNS records programmatically.
  • Panel UI: Register custom pages and menu items in the web control panel.
  • Configuration: Read plugin-specific config values from the main config file.

This surface is intentionally narrow. A plugin can't, for example, directly modify the Nginx configuration or write to the system crontab. Those operations go through the same internal APIs the core itself uses, and we expose them only when there's a clear extension need.

We've expanded the CoreAPI three times since the initial release — once to add billing hooks, once for DNS zone events, and once for custom panel pages. Each expansion required careful thought about what we were committing to support long-term.

Trade-Offs We Accepted#

This architecture has real limitations, and we think it's worth being honest about them.

Go's plugin system is fragile. Plugins must be compiled with the exact same Go version and dependency set as the host binary. A mismatch causes a load failure. This means plugin authors need to pin their Go version and SDK dependency carefully. We mitigate this by shipping a Docker-based plugin build environment, but it's still friction.

No hot-reloading. You can't load or unload plugins without restarting the core process. For a hosting platform that runs as a long-lived daemon, this is usually fine — you restart during a maintenance window. But it rules out certain use cases like A/B testing plugins in production.

Single-process risk. A plugin with a memory leak or a panic affects the entire platform. We run Salieno Core under a process supervisor that restarts it on crash, and we log plugin-caused panics with full stack traces so operators can identify the culprit. But it's not the isolation you'd get from a microservice or subprocess model.

Linux-only. Go's plugin package doesn't work on macOS or Windows. Since Salieno Core targets server deployments on Linux, this hasn't been a practical problem — but it does mean you can't develop plugins on a Mac without cross-compilation.

What We'd Do Differently#

If we started over today, we'd probably still choose shared objects for the core use case, but we'd add a secondary extension path: a lightweight HTTP or Unix socket API for external processes that want to integrate without being in-process. This would let operators write plugins in any language and get process isolation, at the cost of some latency.

We've prototyped this internally and it works well for non-performance-critical hooks like post-provisioning notifications and external billing system sync. Expect to see it in a future release.

The Bottom Line#

Salieno Core's plugin architecture prioritizes simplicity and speed over isolation and flexibility. For the hosting operator who needs to add a custom provisioning step, integrate with a niche billing provider, or extend the control panel with a domain management page, it works well today. For the operator who needs deep, complex integrations with heavy external dependencies, the in-process model has limits.

We think that's an honest trade-off — and one we'll keep evolving as the platform grows.

Share

0 comments

Loading comments…

More from the blog

Inside Salieno Core's Plugin Architecture