TempsTemps
  • Docs
  • Blog
  • Pricing
  • Enterprise
  • Security
  • Contact
Star—
TempsTemps

Open-source deployment platform with built-in error tracking, analytics, and monitoring. Runs on any VPS. No surprise bills, no data leaving your infrastructure.

  • Product
  • Features
  • Documentation
  • Changelog
  • Enterprise
  • Contact
  • Resources
  • Getting Started
  • Upgrade
  • GitHub
  • Reddit
  • Tools
  • VPS Security Scanner
  • PaaS Tax Calculator
  • Compare
  • vs Vercel
  • vs Netlify
  • vs Coolify
  • All Platforms
  • Deploy
  • Next.js
  • Node.js
  • Django
  • Laravel
  • Go
  • Rust
  • All Frameworks →
  • Legal & Compliance
  • Security & Trust
  • Data Ownership & Privacy
  • GDPR Compliance

© 2026 Temps. All rights reserved.

GitHubDocs
t
Temps

4 Plugin Architectures Compared — The One We Chose and Why

4 Plugin Architectures Compared — The One We Chose and Why

March 12, 2026 (4mo ago)

Temps Team

Written by Temps Team

Last updated March 12, 2026 (4mo ago)

Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

#plugins#extensibility#architecture#sidecar#wasm#developer-platform#build plugin system developer platform
Back to all posts

Every successful developer platform hits the same wall. Users ask for a Slack integration. Then Discord. Then PagerDuty, custom webhooks, and five more things you've never heard of. You can't build them all — and you shouldn't try. The answer is a plugin system that lets your community extend the platform while you keep the core lean.

This guide covers the four main plugin architecture patterns, when to use each, and how to build a sidecar-based plugin system from scratch. It finishes with how Temps ships and uses this pattern in production today.


How Do You Build a Plugin System for a Developer Platform?

The fastest path is the sidecar process pattern: plugins run as standalone binaries that your platform spawns as child processes. Communication happens over a local socket (Unix domain socket or WebSocket over one). This gives you real process isolation, any-language support, and latency that disappears compared to the work plugins actually do (network calls, notifications, database writes).

Here is a comparison of all four patterns:

PatternIsolationLatencyLanguage supportSecurityComplexity
In-process (.so/.dll)None~1 nsHost language onlyLowMedium
Scripting engine (WASM/Lua)Strong~10–100 µsMulti (via compile)HighHigh
Sidecar process (HTTP/WebSocket)Strong~50–200 µsAnyHighMedium
Webhook (outbound HTTP)Complete~10–500 msAnyMediumLow

For most developer platforms, sidecar processes hit the sweet spot — real isolation, any-language support, and sub-millisecond dispatch for non-network work.


Why Do Developer Platforms Need a Plugin System?

WordPress powers 43.5% of all websites, and its plugin directory lists over 59,000 free plugins. That ecosystem isn't a side effect — it's the primary reason WordPress dominates. Extensibility turns a product into a platform.

You Can't Build Every Integration

Your users have workflows you'll never anticipate. One team pipes deployment notifications to a Telegram group. Another triggers a Lighthouse audit after every deploy. Someone else needs to update a Jira ticket when a staging environment spins up.

If you try to build all of these yourself, two things happen: your codebase bloats with integration code that serves 2% of users, and your roadmap stalls because you're maintaining fifty connectors instead of improving the core product.

Plugins Keep the Core Product Lean

VS Code has about 30,000 extensions. Kubernetes has hundreds of operators. Grafana has 200+ data source plugins. The pattern repeats because it works: ship a small, stable core and push everything else to the edges.


What Are the Main Plugin Architecture Patterns?

In-Process Plugins (Shared Libraries)

The host loads the plugin directly into its own process as a shared library (.so, .dll, .dylib). Think Nginx modules or Apache httpd modules.

Pros: Near-zero overhead — function calls are nanoseconds. Full access to the host's data structures.

Cons: A segfault in the plugin kills the host. A memory leak degrades the entire process. You're locked to the host's language (or C ABI). No meaningful isolation boundary.

This pattern works for performance-critical infrastructure like proxies. It's a poor fit for a platform where third-party developers write plugins.

Scripting Engine Plugins (Lua, WASM)

The host embeds a scripting runtime and executes plugin code inside a sandbox. Think OpenResty (Lua in Nginx), Envoy (WASM filters), or Shopify Functions (WASM).

Pros: Strong sandboxing — the runtime controls exactly what the plugin can access. WASM can compile from many languages.

Cons: I/O is hard. Network requests, file access, and database queries all need explicit host-provided APIs. The development experience is awkward, and WASM debugging isn't pleasant.

Sidecar Process Plugins (HTTP/WebSocket/RPC)

The plugin runs as its own process. It communicates with the host over a Unix socket, HTTP, or WebSocket. Think HashiCorp's go-plugin library (Terraform, Vault, Consul) or Docker Engine plugins.

Pros: Complete language freedom — write your plugin in Rust, Python, Go, Node, whatever. Process isolation is built in. A crashing plugin doesn't take down the host. Standard debugging tools work.

Cons: Inter-process communication adds latency (50–200 µs per call). You need to manage process lifecycle.

Webhook-Based Plugins

The simplest model. The host fires HTTP requests to registered URLs when events occur. Think GitHub webhooks or Stripe event notifications.

Pros: Dead simple to implement for both you and plugin authors. Language-agnostic. The plugin can run anywhere.

Cons: Fire-and-forget. Network latency dominates. The plugin can't modify the host's behavior inline — it can only react after the fact.


How Does the Sidecar Process Pattern Work?

The sidecar pattern was popularized by HashiCorp's go-plugin library, which powers plugins in Terraform, Vault, Consul, and Packer. The concept is simple: the plugin is a separate binary that speaks a defined protocol.

The critical difference from a microservice is lifecycle coupling — the host starts and stops the plugin. This parent-child relationship is what makes the pattern manageable.

The Core Lifecycle

  1. Discovery — The host scans a plugin directory for binaries
  2. Startup — The host spawns the binary with a socket path and auth secret as CLI args
  3. Handshake — The plugin writes a JSON manifest to stdout; the host reads it
  4. Ready signal — The plugin writes a ready message to stdout after starting its HTTP server
  5. Channel — The host connects to the plugin's WebSocket endpoint for bidirectional event delivery and data access
  6. Operation — The host dispatches events to the plugin over the channel; the plugin processes them and can call back for platform data
  7. Health checking — The host periodically polls the plugin's /health endpoint
  8. Shutdown — The host sends SIGTERM; after a grace period, SIGKILL

The Handshake Protocol

The startup handshake uses stdout for simplicity — no socket negotiation required:

// Plugin writes to stdout (phase 1): manifest declaration
{
  "type": "manifest",
  "name": "slack-notifier",
  "version": "1.0.0",
  "display_name": "Slack Notifier",
  "description": "Sends deployment notifications to Slack",
  "events": ["deployment.succeeded", "deployment.ready"],
  "health_path": "/health"
}

// Plugin writes to stdout (phase 2): ready signal after HTTP server starts
{
  "type": "ready",
  "ready": true,
  "has_ui": false
}

After the ready signal, the host connects to the plugin's WebSocket channel at /_temps/channel for bidirectional communication.

The Channel Protocol

The WebSocket channel carries three message types:

// Plugin → Platform: request platform data
{
  "type": "request",
  "id": 1,
  "method": "get_project",
  "params": { "project_id": 42 }
}

// Platform → Plugin: response
{
  "type": "response",
  "id": 1,
  "result": { "name": "my-app", "slug": "my-app" }
}

// Platform → Plugin: pushed event (fire-and-forget)
{
  "type": "event",
  "event": {
    "id": "uuid",
    "event_type": "deployment.succeeded",
    "timestamp": "2026-06-06T12:00:00Z",
    "project_id": 42,
    "data": {
      "deployment_id": 100,
      "url": "https://myapp.example.com",
      "commit_sha": "a1b2c3d"
    }
  }
}

This design separates concerns cleanly: the channel handles data access and event delivery, while the plugin's own HTTP routes handle user-facing API calls proxied from the platform.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

How Should You Design the Plugin API?

The Plugin Manifest

Every plugin declares a manifest during the handshake. The manifest tells the host what events to deliver, what nav entries to add to the UI, and whether the plugin needs a database:

// Rust SDK — PluginManifest builder
PluginManifest::builder("slack-notifier", "1.0.0")
    .display_name("Slack Notifier")
    .description("Sends deployment notifications to Slack")
    .event("deployment.succeeded")
    .event("deployment.ready")
    .nav(NavEntry {
        label: "Slack".into(),
        icon: "message-square".into(),
        section: NavSection::Settings,
        path: "/slack".into(),
        order: 60,
    })
    .requires_db(false)
    .build()

A few design decisions matter. The events field is a whitelist — the host only delivers events the plugin declared. Wildcards like "deployment.*" work too. Nav entries with section: NavSection::Platform appear in the main sidebar; Settings places them in the admin section.

Event Types

Define a clear set of lifecycle events. Start with the ones your users actually ask about. In Temps, the supported events are:

  • deployment.succeeded — A deployment is live and healthy
  • deployment.ready — A deployment passed health checks and is serving traffic
  • project.created — A new project was registered
  • project.deleted — A project was removed
  • deployment.* — Wildcard matching all deployment events

Each event carries a typed payload. Include only what a plugin needs: IDs, timestamps, URLs, status codes. Don't dump your entire internal state.

Route Namespacing

Plugins own their own HTTP routes under a namespaced prefix. In the Temps architecture, routes at /api/x/{plugin_name}/* are proxied to the plugin process. This means plugin authors write standard axum (or Express, FastAPI, etc.) routes without thinking about routing collisions.


What Are the Critical Security Considerations?

Plugin systems are insecure-by-design magnets — you're running someone else's code on your infrastructure. Layer your defenses.

Process Isolation

Run each plugin as its own process with a dedicated auth secret. The auth secret is passed as a CLI argument at spawn time — plugin routes can validate the Authorization header against it to reject unauthorized callers.

# Temps spawns plugins with:
/path/to/plugin-binary \
  --socket-path /var/run/temps/plugins/my-plugin.sock \
  --auth-secret <generated-per-spawn> \
  --data-dir /var/lib/temps/plugins/my-plugin/

For stronger isolation, use cgroups v2 and namespaces to give each plugin its own view of the filesystem, process tree, and network stack.

Resource Limits

Set hard ceilings on what each plugin can consume:

# Using systemd resource controls
[Service]
MemoryMax=256M
CPUQuota=25%
TasksMax=64
LimitNOFILE=1024

Without limits, a single misbehaving plugin can exhaust memory, CPU, or file descriptors — and every service on the box suffers.

Network Restrictions

Plugins should never reach your internal database or admin API directly. Use iptables rules or network namespaces to:

  • Allow: External HTTPS (ports 443, 80) for calling third-party APIs
  • Block: Localhost connections to your database, admin API, or other plugin sockets

Audit Logging

Log every plugin lifecycle event (started, stopped, crashed), every event dispatched and the response received, and resource usage snapshots at regular intervals. This is your forensic record when something goes wrong.


Free guide

Zero-Downtime Deployment Playbook

The deployment checklist used by teams shipping 10+ times per day without dropping a single request.

  • Blue-green vs rolling vs canary — when to use each
  • Health check configuration that actually catches failures
  • Preview environment setup for every PR
  • Automated rollback triggers and procedures

No spam. Unsubscribe anytime. Privacy policy

How Do You Build a Minimal Plugin System?

Here is a working implementation covering discovery, lifecycle management, handshake, and event dispatch — under 120 lines of Rust:

Plugin Discovery

Scan a directory for plugin binaries:

use std::fs;
use std::path::{Path, PathBuf};

fn discover_plugins(dir: &Path) -> Vec<PathBuf> {
    let mut plugins = Vec::new();
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            // Any executable file in the plugins directory is a plugin
            if path.is_file() {
                #[cfg(unix)]
                {
                    use std::os::unix::fs::PermissionsExt;
                    if let Ok(meta) = path.metadata() {
                        if meta.permissions().mode() & 0o111 != 0 {
                            plugins.push(path);
                        }
                    }
                }
            }
        }
    }
    plugins
}

Lifecycle Management and Handshake

Spawn each plugin and read its manifest from stdout:

use std::process::{Command, Stdio, Child};
use std::io::{BufRead, BufReader};
use std::collections::HashMap;
use serde::Deserialize;

#[derive(Deserialize)]
struct PluginManifest {
    name: String,
    version: String,
    events: Vec<String>,
    health_path: Option<String>,
}

struct RunningPlugin {
    child: Child,
    manifest: PluginManifest,
    socket_path: String,
}

struct PluginManager {
    plugins: HashMap<String, RunningPlugin>,
    socket_dir: String,
    auth_secret: String,
}

impl PluginManager {
    fn start_plugin(&mut self, binary: &std::path::Path) -> std::io::Result<()> {
        let plugin_name = binary.file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("unknown")
            .to_string();
        let socket_path = format!("{}/{}.sock", self.socket_dir, plugin_name);
        let data_dir = format!("/var/lib/temps/plugins/{}", plugin_name);

        let mut child = Command::new(binary)
            .arg("--socket-path").arg(&socket_path)
            .arg("--auth-secret").arg(&self.auth_secret)
            .arg("--data-dir").arg(&data_dir)
            .stdout(Stdio::piped())
            .stderr(Stdio::inherit())
            .spawn()?;

        // Read manifest from stdout (handshake phase 1)
        let stdout = child.stdout.take().expect("stdout piped");
        let mut reader = BufReader::new(stdout);
        let mut line = String::new();
        reader.read_line(&mut line)?;

        // Strip the "type":"manifest" wrapper if present
        let manifest: PluginManifest = serde_json::from_str(line.trim())
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        // Read ready signal (handshake phase 2)
        line.clear();
        reader.read_line(&mut line)?;
        // { "type": "ready", "ready": true }

        self.plugins.insert(plugin_name, RunningPlugin {
            child,
            manifest,
            socket_path,
        });
        Ok(())
    }

    fn stop_plugin(&mut self, name: &str) {
        if let Some(mut plugin) = self.plugins.remove(name) {
            let _ = plugin.child.kill();
        }
    }
}

Event Dispatch

Send events to plugins that subscribed to the relevant event type. In production you'd use the WebSocket channel; here is the simplified HTTP POST pattern for illustration:

async fn dispatch_event(
    client: &reqwest::Client,
    base_url: &str,
    event_type: &str,
    payload: &serde_json::Value,
) -> Result<(), reqwest::Error> {
    let event = serde_json::json!({
        "id": uuid::Uuid::new_v4().to_string(),
        "event_type": event_type,
        "timestamp": chrono::Utc::now().to_rfc3339(),
        "data": payload,
    });

    client.post(format!("{base_url}/_events"))
        .json(&event)
        .send()
        .await?;
    Ok(())
}

That is the foundation. Add WebSocket channel handling, health check polling, restart logic, and cgroup resource limits to reach production quality.


How Does Temps Implement External Plugins?

Temps ships a complete external plugin system based on this sidecar pattern. Plugins are standalone binaries placed in ~/.temps/plugins/ — Temps discovers, spawns, and manages their lifecycle automatically on startup.

Three Quotable Temps Differentiators

  1. Embedded UI: Plugins can embed a full React UI at compile time (include_dir!("web/dist")). Temps proxies /api/x/{plugin_name}/ui/* to the plugin and renders it in an iframe inside the console — no separate deployment required.
  2. Bidirectional channel: The platform-to-plugin WebSocket channel at /_temps/channel lets plugins query platform data (projects, deployments, environments) without direct database access — clean capability boundary by design.
  3. Single Rust binary, ~$6/mo self-hosted: Temps replaces Vercel, PostHog/Plausible, FullStory, Sentry, Pingdom, managed databases, and transactional email in one process. The plugin system extends it further, Apache 2.0.

How Plugins Are Installed

Drop a plugin binary into the plugins directory and restart Temps:

# Download an official plugin
curl -L https://github.com/gotempsh/temps/releases/latest/download/indexnow-plugin-linux-amd64 \
  -o ~/.temps/plugins/indexnow-plugin
chmod +x ~/.temps/plugins/indexnow-plugin

# Temps auto-discovers it on next startup
# Or install Temps itself first:
curl -fsSL temps.sh/install.sh | bash

How Temps Uses the Rust Plugin SDK

Official plugins use the temps-plugin-sdk crate. The main! macro and run_plugin() function handle everything: stdout handshake, axum HTTP server on the Unix socket, WebSocket channel, /health endpoint, SIGTERM handler. Plugin authors implement one trait:

use temps_plugin_sdk::prelude::*;

struct SlackNotifier;

impl ExternalPlugin for SlackNotifier {
    fn manifest(&self) -> PluginManifest {
        PluginManifest::builder("slack-notifier", "1.0.0")
            .display_name("Slack Notifier")
            .description("Sends deployment notifications to Slack")
            .event("deployment.succeeded")
            .build()
    }

    fn router(&self, ctx: PluginContext) -> axum::Router {
        axum::Router::new()
            .route("/settings", get(get_settings).patch(update_settings))
            .with_state(ctx)
    }

    fn on_event(&self, ctx: &PluginContext, event: PluginEvent) {
        if event.event_type != "deployment.succeeded" {
            return;
        }
        let url = event.data.get("url")
            .and_then(|v| v.as_str())
            .unwrap_or_default();
        // Post to Slack webhook, etc.
        tracing::info!(url, "Deployment succeeded — notifying Slack");
    }
}

temps_plugin_sdk::main!(SlackNotifier);

Python and Node.js Plugins

The protocol is language-agnostic. Python and Node.js SDKs follow the same two-phase stdout handshake:

# Python plugin (using temps-plugin Python SDK)
from temps_plugin import PluginManifest, NavEntry, emit_manifest, emit_ready, run_server

manifest = PluginManifest(
    name="slack-notifier",
    version="1.0.0",
    display_name="Slack Notifier",
    description="Sends deployment notifications to Slack",
    events=["deployment.succeeded"],
)

emit_manifest(manifest)
# ... start your HTTP server ...
emit_ready(has_ui=False)
# ... serve requests

All the platform asks of a plugin is: write your manifest to stdout, write a ready signal, then listen for events. No special framework required.

Official Plugins

Official Temps plugins — IndexNow (instant search engine URL submission), Google Indexing API, Lighthouse audits, and a minimal example — are open source at github.com/gotempsh/temps in the examples/ directory.


Frequently Asked Questions

How Can I Add a Plugin or Extension System to My Self-Hosted Deployment Platform / PaaS?

One practical design is a sidecar process: the platform starts each plugin as a separate executable, reads a manifest during a startup handshake, and communicates over a local socket. Temps uses this design for external plugins. By default, executables live under the Temps data directory in plugins/ (~/.temps/plugins/ for the default data directory). Temps connects to the plugin's /_temps/channel WebSocket endpoint for events and platform-data requests, and mounts plugin HTTP routes under /api/x/{plugin_name}. Start with a small lifecycle-event allowlist such as deployment.succeeded and project.created, define a versioned manifest, and apply process-level resource limits before expanding the API.

Should I Use WASM or Sidecar Processes for Plugins?

It depends on your threat model and performance requirements. WASM gives stronger sandboxing and near-native speed, but the developer experience is rough — I/O requires host-provided APIs, and debugging is painful. Sidecar processes let plugin authors use any language with standard tooling and debuggers. For most developer platforms, sidecars are the pragmatic choice. WASM makes sense if you're executing untrusted code in a request's hot path, like edge compute functions.

How Do I Handle Plugin Versioning and Updates?

Use semantic versioning for your plugin API. The manifest's declared API version is your compatibility contract. When you release a breaking change to the event payload format, bump the major version. Old plugins keep working against the old API version until you deprecate it. For plugin binary updates, treat them like any other deployment: download, validate checksum, swap, restart.

What's the Performance Overhead of a Plugin System?

Unix socket roundtrips add 50–100 µs per event dispatch. For comparison, a typical HTTP API call to Slack takes 200–800 ms. The plugin overhead is noise. The real performance cost is spawning processes — keep plugins running as daemons rather than starting a new process per event.

How Do I Prevent Malicious Plugins From Causing Damage?

Layer your defenses. Run each plugin as its own OS user with minimal permissions. Use cgroups to cap memory and CPU. Restrict network access with namespaces or firewall rules — block localhost, allow only external HTTPS. Log every event dispatch and response. Never give plugins access to host database credentials or internal API keys. Inject only the specific config values each plugin declares in its manifest.

How Do Plugins Access Platform Data Without Direct Database Access?

In the Temps architecture, plugins request platform data over the bidirectional WebSocket channel (/_temps/channel). The plugin sends a request message with a method like get_project or list_environments; the platform responds with only what the plugin is authorized to see. This gives plugins rich data access without any direct database coupling.


Build Your Platform's Extension Layer

A plugin system isn't just a feature — it's a multiplier. It turns your users into contributors, keeps your core lean, and makes your platform adaptable to workflows you haven't imagined yet.

The sidecar process pattern gives you the best foundation for most developer platforms: real isolation, any-language support, and latency that disappears compared to the work plugins actually do. Start with a handful of lifecycle events, a clear manifest format, and strict resource limits. Expand the API surface as your community tells you what they need.

Temps ships a complete external plugin system using exactly this pattern — sidecar processes over Unix sockets with a WebSocket channel, managed by the same daemon that handles your deployments, Apache 2.0. Try it on your own infrastructure at ~$6/mo on Temps Cloud (Hetzner cost + 30%, no per-seat fees) or self-host for free:

curl -fsSL temps.sh/install.sh | bash