Skip to content
Super 1.7.0 is production-ready — transactional installs with automatic rollback, cron runtime caps (kill_after_secs), and panic-free input handling. See what’s new →

Design Philosophy

Project Super was built to solve specific architectural shortcomings in existing process managers like Supervisor (Python) and PM2 (Node.js). We don’t just manage processes; we engineer for edge cases, memory constraints, and high-concurrency storms.

Here is an inside look at the architectural decisions that make Super fundamentally different.

System architecture (overview)

Super is a single static binary (superd) that runs on your server or container. You configure it with super.toml, manage programs through the CLI or HTTP API, and optionally unlock subscription features by adding a signed license key and official plugin libraries — no separate “enterprise” build.

    flowchart TB
  subgraph operators["Operators & automation"]
    CLI["super CLI"]
    CI["CI / scripts"]
    Browser["Browser optional"]
  end

  subgraph superd["superd (OSS core)"]
    API["REST API /api/v1/*"]
    WS["WebSocket /ws logs"]
    MET["/metrics Prometheus"]
    MGR["Process Manager\nActor + registry"]
    EVTW["Event batch writer\n(non-blocking SQLite flush)"]
    HOOKS["Event hooks\n[[event_hooks]]"]
    API --> MGR
    WS --> MGR
    MET --> MGR
    MGR -->|try_send| EVTW
    MGR --> HOOKS
  end

  subgraph optional["Optional plugins runtime"]
    SEC["security\nTokens · RBAC · audit"]
    UI["ui\nPro UI extensions"]
    NOT["notify\nWebhooks · IM"]
    ISO["isolation\ncgroups Linux"]
  end

  subgraph data["On disk SUPER_ROOT"]
    CONF["conf/super.toml"]
    SNAP["data/snapshot.json\nprogram registry"]
    EVDB["data/events.db\nSQLite event history"]
    LOGS["logs/\ndaemon + child"]
    RUN["run/\nsocket · pidfile"]
    PLUG["plugins/*.so"]
    AUTH["data/tokens.json\nLicensed tokens"]
  end

  subgraph workloads["Managed programs"]
    P1["app A"]
    P2["app B"]
    P3["cron / worker"]
  end

  CLI --> API
  CI --> API
  Browser --> API
  Browser --> UI

  CONF --> superd
  SNAP --> MGR
  LOGS --> MGR
  PLUG --> optional
  optional --> superd
  AUTH -.->|Licensed| SEC

  EVTW -->|batch INSERT| EVDB
  MGR -->|query / stats| EVDB
  API -->|GET /events*| MGR

  MGR -->|SystemEvent| NOT
  MGR --> P1
  MGR --> P2
  MGR --> P3
  P1 --> LOGS
  P2 --> LOGS
  

How the pieces fit together

ComponentRole
superdLong-running daemon: spawns processes, health checks, OTA updates, persistence, API. Runs in the foreground by default; optional Unix --daemon / [server] daemon when not under systemd/Docker.
super CLILocal or remote control against the API (create/start/logs/stack). Same commands whether superd is foreground or self-daemonized.
REST + WebSocketDeclarative control, live log streaming, and event history queries (GET /api/v1/events, /events/stats).
/metricsPrometheus scrape endpoint (OSS).
conf/super.tomlDaemon settings (including optional daemon / pidfile, [[event_hooks]]), optional [license].key, optional auth_secret.
data/snapshot.jsonDurable program registry (atomic writes + .bak recovery). See Snapshot persistence.
data/events.dbSQLite event history (WAL mode): all lifecycle events — crashes, recoveries, cron runs, daemon startup/shutdown. The Manager queues records to a background batch writer so the actor loop never blocks on disk; retention is configurable via [storage] events_keep_days. See Event History.
logs/Daemon (app.log) and child process stdout/stderr (rotation configurable). Paths resolve under SUPER_ROOT.
run/Optional Unix socket (run/superd.sock) and pidfile when self-daemonizing.
data/tokens.jsonLicensed: Access Token hashes (security plugin).
data/auth_settings.jsonLicensed: auth_secret_disabled and related security flags.
PluginsOptional .so / .dylib loaded at runtime after license verification. notify subscribes to the same SystemEvent stream as OSS hooks.

OSS (default): loopback-first bind with an embedded Dashboard; API open on loopback unless auth_secret is set. Non-loopback binds require auth_secret or refuse to start. See Authentication and Configuration.

Subscription: same superd binary; add [license].key, deploy plugin libraries under plugins/, and set auth_secret. The security plugin is included with every subscription and is required for licensed startup — API token auth and RBAC then protect the control plane. See Authentication and the Feature matrix.

Typical control flow: an operator or pipeline sends POST /api/v1/programs (or super add) → the Manager validates config → spawns the child in its own process group → streams stdout/stderr to disk and WebSocket → records lifecycle events to the SQLite queue (batch-flushed to events.db) → health probes and dependency rules decide when downstream services start. Shutdown and OTA paths use the same Manager mailbox so state stays consistent under load.

Event flow (separate from program config): every lifecycle signal is emitted as a SystemEvent → optionally forwarded to [[event_hooks]] or the licensed notify plugin in real time → always queued for persisted history in events.db (query later via super events or the events API). See Events.

For day-to-day configuration rather than internals, start with Configuration and Quick Start.

1. Why Rust?

We chose Rust not for the hype, but for Memory Safety and Predictability.

  • Zero GC Pauses: Unlike Go, Java, or Node.js, Rust has no Garbage Collector. This ensures the supervisor process never “freezes” for a few milliseconds to clean up memory—a critical property when monitoring high-frequency trading apps or real-time control systems.
  • Memory Footprint: A statically compiled Super binary (via musl with LTO and stripped symbols) typically consumes < 5MB of RAM. This is crucial for edge devices with 256MB RAM where every megabyte counts.
  • Concurrency: Rust’s “Fearless Concurrency” allows us to handle log streams, health checks, and API requests in parallel without race conditions.

2. OS Internals & Lifecycle Governance

Process Groups (PGID) over Single PIDs

Why do we kill by Process Group (-PID) instead of just the main PID? If a user script (start.sh) spawns a Java process and Super only kills the shell script, the Java process becomes an “orphaned” zombie holding onto port 8080. Before executing a payload, Super sets a new process group (process_group(0)). During shutdown, we send SIGTERM to the negative PID (kill(-PID, SIGTERM)), effectively broadcasting the signal to the entire process tree, leaving zero orphans.

Applications must not break out of this group — see the Managed Program Requirements.

The 10-Second SIGKILL Timeout

We support graceful shutdowns, but we never trust user code implicitly. If a process catches SIGTERM but enters a deadlock or refuses to exit, it could hang the manager indefinitely. Super isolates this by spawning a 10-second detached timer. If the process is still alive when the timer pops, Super ruthlessly issues a SIGKILL to the process group, ensuring system resources are always reclaimed.

Differentiating “Crashes” from “Completed Jobs”

Older process managers assume any exit means a crash. Super respects POSIX standards:

  • Exit Code 0: Treated as a successful completion (Stopped). It does not trigger a restart. This makes Super perfect for Cron jobs and one-off tasks (like database migrations).
  • Exit Code != 0: Treated as a Crash. Super triggers an exponential backoff retry and fires an alert.

3. Concurrency & The Actor Model

Super uses an Actor-like architecture based on Tokio mpsc channels.

  • Single Source of Truth: There is only one owner of the system state (ProcessRegistry) running in a single event loop. All external actions (API requests, Health Checks, Exits) are converted into Command enums and sent to the Manager’s mailbox.
  • Non-blocking event persistence: Lifecycle events are not written synchronously in the actor loop. The Manager try_sends each record to a dedicated background task that batch-inserts into SQLite (events.db). If the queue is full, new records are dropped rather than blocking process control — the actor never waits on disk I/O.
  • Preventing Deadlocks: The core mailbox has a massive capacity (2048). Furthermore, non-critical background tasks (like periodic CPU/Memory metric collection) use try_send instead of blocking. If the manager is saturated under a massive API load, metrics are silently dropped rather than causing a system-wide deadlock.
  • Anti-Avalanche (Staggered Startup): When a server reboots, starting 30 microservices simultaneously causes a massive CPU/IO spike, often leading to health-check timeouts and crash loops. Super implements Staggered Startup (Jitter). It pauses for 100ms between each process spawn, smoothing out the IO spike and bringing the entire cluster online gracefully.

4. Security & Crash Safety

How does Super guarantee safety during sudden kernel panics or power losses? We borrowed concepts from database design and Unix philosophy.

Transactional OTA & WAL (Write-Ahead Log)

When performing an over-the-air binary update, we write a WAL Record (the restore_path) to disk before touching the binary. If power is lost mid-update, Super reads the WAL upon reboot, notices an unfinished transaction, and triggers an automatic rollback to the known-good backup.

Atomic State Swaps

When saving state to snapshot.json, Super writes to a .tmp file, calls fs::sync(), and then performs an atomic fs::rename(). The config file is never corrupted, even during a sudden power loss.

SQLite Event History

Program lifecycle events are stored in data/events.db (SQLite, WAL journaling mode — distinct from the OTA restore WAL file). All events are recorded with millisecond timestamps; optional events_keep_days prunes old rows once per UTC day. Indexes align with the (ts_ms, id) sort key used by the events API so large histories stay queryable without loading everything into RAM. Path is configurable via [storage] events_file (relative paths resolve under SUPER_ROOT).

Secret Mounting & Display Masking

We do not use master-key encryption for secrets, as rotating the key would permanently destroy configurations (Crypto-shredding). Instead, we use a Reference & Masking architecture:

  1. Super allows mounting .env files dynamically. The secrets never touch snapshot.json.
  2. When accessed via CLI or API, environment variables whose keys contain PASSWORD, SECRET, TOKEN, KEY, or CREDENTIAL are automatically masked as ******** to prevent shoulder-surfing.
  3. As a final fallback, Super forces 0600 file permissions on snapshot.json, ensuring only the root user can read the underlying configuration.

5. Defensive Programming: Beating OOM Attacks

Chunked Log Streams

Traditional log reading uses BufReader::lines(), which stores data in RAM until it finds a newline \n. If a misbehaving or malicious app outputs an infinite stream of binary data without newlines, the daemon will suffer an OOM (Out of Memory) crash within seconds.

Super utilizes a custom, fixed-size chunk reader. It strictly enforces a 16KB limit per line. If a line exceeds this, Super forcibly truncates it, flushes it to disk with a ...[TRUNCATED] suffix, and immediately frees the memory. Super’s RAM footprint remains flat, regardless of how much garbage the child processes output.

Was this page helpful? Thanks for your feedback!

Still have questions? Open an issue or browse the source.