Plugin and Event Infrastructure for Elixir cities

Wire your application like a city.

Every plugin becomes a supervised tower. Every event becomes a priority-routed pipe. Every halt is a junction valve. Mishka Installer is the plugin and event manager that runs the underground of your Elixir application, wired with Erlang OTP and lit by sodium-lamp telemetry.

v0.0.0 Hex ↗
EVENT BUS PLUGIN A SUPERVISOR PLUGIN B GENSERVER OTP HUB PLUGIN HOTEL EVENT BAR priority routing · 24h "after_register" "after_login" "after_purchase"
City Bus · live · 3 events
What is Mishka Installer?

A plugin & event manager built on OTP.

Mishka Installer is an open-source Elixir library for adding behavior to your application as plugins, without touching its core code. Every plugin you register becomes its own supervised GenServer process. It tracks its own status, can be switched off and on at runtime, and can report its health.

Plugins attach to named events ("after_register", "after_login", anything you invent) and the dispatcher runs them in priority order, a plugin's depends first. Each plugin's reply is the next plugin's input, and any plugin can halt the pipeline; the rest are skipped. An optional sealed :private payload is merged into a map or keyword result and returned to the caller.

It is also a runtime library installer: Installer.install/1 loads a pre-built library (a compiled ebin from a local path, a URL or a GitHub release) into the running node, with an allowlist for remote sources and an optional sha256 checksum.

Tower per plugin
Every plugin is its own OTP GenServer. It tracks its status, switches off and on at runtime, and can report its health.
01
Pipe per event
Named events run every active plugin in priority order, 0 to 100. The smaller number runs first, and a plugin's depends run before it.
02
Junction valves
Return {:reply, :halt, _} to stop downstream plugins from running, like closing a valve.
03
Sealed payloads
Pass :private data alongside the event. No plugin sees it; it is merged into a map or keyword result.
04
Blueprint #01

Anatomy of a plugin tower.

Pinned to the steel wall, blueprint pen on rivets. Five wires, one purpose.

Draft · Sheet 01 / 03
defmodule RegisterEmailSender do  use MishkaInstaller.Event.Hook,    event: "after_success_login",    initial: %{depends: [OtherPlugin], priority: 20}  @impl true  def call(entries) do    {:reply, entries}  endend

▸ Select a part to light up its line of code.

A night in the city

What happens at 21:04?

One registration · three towers · one valve closure. Scrub the sequence below.

DISPATCH CLOCK
frame 1 / 5
21:04:02
tty/mishka.installer · 9600 baud UID: req_4f-7ab
▸ 21:04:02.117 WEB [INFO] POST /register · params accepted
21:04:02.124 HOST [INFO] Hook.call! → "after_register" fired
21:04:02.131 PIPE [ROUTE] Dispatcher → SendOTPPlugin · prio 20
21:04:02.198 OTP [HALT] Reply :halt received · downstream cancelled
21:04:02.199 HOST [OK] Caller resumed with the halt reply
EXEC %{email: "ada@..."}
signal ● WEB
The trunk line

Priority routes the flow. A halt closes the valve.

Watch a packet enter the trunk pipe, ride through three plugin junctions, and get cut short by an inline valve.

EVENT "after_register"
SendOTPPlugin prio 20 queued SendEmailPlugin prio 50 queued AuditLogPlugin prio 100 queued
↳ event fired · packet entering trunk…
The substation

Five generators, five power states.

Every plugin has one of these statuses. Read it with Event.get(:name, MyPlugin).

⊕
Registered
:registered
Pinned to the registry, not yet powered up.
▶
Started
:started
Generator online. Events flowing through.
■
Stopped
:stopped
Switched off; stays off on reboot.
↻
Restarted
:restarted
Restarted, or switched back on with enable/0. Runs like started.
❘❘
Held
:held
Waits until its depends are active.
Spec sheet · sheet 02

Reply patterns for call/1.

A maintenance reference: what each reply means, whether it merges with :private, and whether it halts the line. A reply that is not {:reply, state} or {:reply, :halt, state} is logged and the event returns its input unchanged.

MAINTENANCE NOTE Approved by: shahryarjb · rev 02.18
Return shape
Mergeable
Halts
Notes
{:reply, {:ok, data}} ✕ no ○ continues not a map or list
{:reply, {:ok, [name: "..."]}} ✓ yes ○ continues keyword list
{:reply, {:ok, %{name: "..."}}} ✓ yes ○ continues map
{:reply, {:error, errors}} ✕ no ○ continues error path
{:reply, [name: "..."]} ✓ yes ○ continues keyword list
{:reply, %{name: "..."}} ✓ yes ○ continues map
{:reply, :halt, {:ok, %{...}}} ✓ yes ◼ closes line —
{:reply, :halt, {:error, errors}} ✕ no ◼ closes line halts with errors
Valve types

Three ways to open the line.

Hook.call/3 takes private: and return: options. A third, meta:, hands read-only context to plugins that define call/2.

VALVE 01
Normal

Open valve: params flow through every plugin and come back modified.

Hook.call("after_login", params)
:private
VALVE 02
:private payload

A sealed bypass line skips every plugin and is merged into a map or keyword result.

Hook.call("after_login", params,
  private: %{request_id: rid})
VALVE 03
return: true

A return-pipe runs the plugins, then hands the caller its original input.

Hook.call("after_login", params,
  return: true)
Night shift · live dispatch

Take the operator's chair.

Pick a tower, hit a switch. A simulation of the plugin registry that follows the library's statuses and rules.

DISPATCH BOARD · operator: shahryarjb 5 plugins · 3 events
after_register (2)
after_login (2)
after_purchase (1)
SendOTPPlugin
on "after_register" · prio 20
started
↳ try "Fire event" to dispatch through the pipe
TELEMETRY live
21:03:58 [BOOT] Dispatch board online. 5 towers under supervision.
City Dispatch · monthly broadcast

Tune in to the city dispatch.

One transmission a month: new patterns from the trunk line, dispatcher tricks, plugin recipes. No siren spam. Cut the line in one keystroke.

● MONTHLY● NO SIRENS● ONE-KEY CUT