Plushie is configured at three levels: environment variables for
deployment and CI, application config for project-wide defaults, and
runtime options on Plushie.start_link/2 for per-instance control.
Most projects need minimal configuration. Build or download the
binary and go.
Environment variables
| Variable | Purpose |
|---|---|
PLUSHIE_BINARY_PATH | Explicit path to the renderer binary. Overrides all other binary resolution. |
PLUSHIE_SOURCE_PATH | Path to a local plushie-renderer checkout for source builds. |
PLUSHIE_TEST_BACKEND | Test backend: mock (default), headless, or windowed. |
PLUSHIE_UPDATE_SCREENSHOTS | When set to any value, update screenshot golden files instead of comparing. |
PLUSHIE_UPDATE_SNAPSHOTS | When set to any value, update tree-hash snapshot files instead of comparing. |
RUST_LOG | Renderer log verbosity. Set to plushie=debug for full protocol logging. Overrides the :log_level runtime option when set. |
PLUSHIE_BINARY_PATH
Forces a specific renderer binary. Useful in CI where the binary is pre-built and stored at a known path, or in production deployments where the binary ships alongside the release. If set but pointing to a missing file, resolution raises immediately rather than falling through to other paths.
PLUSHIE_SOURCE_PATH
Points to a local checkout of the
plushie-renderer
repository. When set, mix plushie.build uses local path dependencies
instead of crates.io, and the dev server watches .rs files for hot
reload. The typical setup is a sibling directory:
git clone https://github.com/plushie-ui/plushie-renderer ../plushie-renderer
PLUSHIE_SOURCE_PATH=../plushie-renderer mix plushie.build
RUST_LOG
Controls the renderer's log output using the standard
env_logger format. The Bridge forwards
this to the renderer process at startup. Common values:
plushie=debug- full protocol and rendering detailplushie=info- connection events and major state changesplushie=warn- warnings only (default-like)
When RUST_LOG is set, the :log_level option on start_link/2 is
ignored. Combine with --json for full wire protocol visibility:
RUST_LOG=plushie=debug mix plushie.gui MyApp --json
Application config
Set in config/*.exs under config :plushie. A typical development
setup looks like this:
# config/dev.exs
import Config
config :plushie, code_reloader: trueMost projects only need :code_reloader in dev. The remaining options
are for customising builds, binary paths, and test backends.
Binary and build
| Key | Type | Default | Purpose |
|---|---|---|---|
:binary_path | String.t() | nil | Explicit binary path. The env var PLUSHIE_BINARY_PATH takes precedence if set. |
:source_path | String.t() | nil | Rust source checkout path. The env var PLUSHIE_SOURCE_PATH takes precedence if set. |
:build_name | String.t() | "app-renderer" | Custom binary name for mix plushie.build output. |
:build_profile | :release | :debug | :debug | Cargo build profile. :release enables optimisations. |
:artifacts | [:bin] | [:bin, :wasm] | [:bin] | Which artifacts mix plushie.build and mix plushie.download install. |
:bin_file | String.t() | nil | Override binary output path for build/download. |
:wasm_dir | String.t() | nil | Override WASM output directory for build/download. |
Development
| Key | Type | Default | Purpose |
|---|---|---|---|
:code_reloader | boolean() | keyword() | false | Enable hot code reloading in dev. |
:test_backend | :mock | :headless | :windowed | :mock | Test backend selection. |
Code reloader options
When :code_reloader is set to a keyword list, these options are
supported:
| Key | Type | Default | Purpose |
|---|---|---|---|
:debounce_ms | integer() | 100 | Milliseconds to wait after a file change before recompiling. Prevents rapid recompilation during multi-file saves. |
:rebuild_artifacts | [:bin] | [:bin, :wasm] | [:bin] | Which Rust artifacts to rebuild when .rs files change. Add :wasm to also rebuild the WASM renderer. |
:dirs | [String.t()] | Mix project's elixirc_paths | Directories to watch for Elixir file changes. Override if your source lives outside the standard paths. |
Rust file changes (.rs, Cargo.toml) use a separate debounce of
200ms (not configurable) because Cargo builds are heavier and benefit
from more coalescing.
Runtime options (start_link/2)
Plushie.start_link/2 starts a Plushie application. The first argument
is the app module (implementing Plushie.App), and the second is an
optional keyword list of options:
Plushie.start_link(MyApp, binary: Plushie.Binary.path!(), daemon: true)Common options
| Option | Type | Default | Purpose |
|---|---|---|---|
:app_opts | term() | [] | Forwarded to app.init/1. Use this to pass startup configuration to your app. |
:binary | String.t() | auto-resolved | Path to the renderer binary. Usually resolved automatically via Plushie.Binary.path!/0. |
:name | atom() | Plushie | Supervisor registration name. The Runtime registers as {name}.Runtime, the Bridge as {name}.Bridge. |
:daemon | boolean() | false | Keep running after the last window closes. In both modes, update/2 receives %SystemEvent{type: :all_windows_closed}. Without daemon, the runtime shuts down after that update. With daemon, it continues running so you can open new windows. |
Transport
| Option | Type | Default | Purpose |
|---|---|---|---|
:transport | :spawn | :stdio | {:iostream, pid} | :spawn | How the SDK communicates with the renderer. See transport modes. |
:format | :msgpack | :json | :msgpack | Wire protocol format. JSON is human-readable, useful for debugging. |
Advanced
| Option | Type | Default | Purpose |
|---|---|---|---|
:code_reloader | boolean() | keyword() | false | Override the config-level code_reloader setting for this instance. |
:log_level | atom() | :error | Renderer log level (:error, :warn, :info, :debug). Ignored if RUST_LOG is set. |
:renderer_args | [String.t()] | [] | Extra CLI arguments appended to the renderer command. Only applies in :spawn transport mode. |
App settings callback
The optional Plushie.App.settings/0 callback provides
application-level defaults to the renderer at startup. These are
sent once during the initial handshake and affect all windows.
defmodule MyApp do
use Plushie.App
import Plushie.UI
def settings do
[
default_text_size: 16,
theme: :dark,
fonts: ["priv/fonts/inter.ttf"],
default_event_rate: 60
]
end
def init(_opts), do: %{}
def update(model, _event), do: model
def view(_model) do
window "main", title: "MyApp" do
text("hello", "Hello")
end
end
end| Key | Type | Default | Purpose |
|---|---|---|---|
default_font | map() | system default | Default font specification (same format as font props). |
default_text_size | number() | renderer default | Default text size in pixels for all text widgets. |
antialiasing | boolean() | true | Enable font antialiasing. |
vsync | boolean() | true | Synchronize frame presentation with the display refresh rate. Disabling can improve performance on some platforms. |
scale_factor | number() | 1.0 | Multiplier on top of OS DPI scaling. Setting 2.0 on a 2x HiDPI display gives 4x physical pixels per logical pixel. Per-window overrides are available via the scale_factor window prop. |
theme | atom() | map() | renderer default | Built-in theme (:dark, :light, :nord, :system, etc.) or a custom palette. See Plushie.Type.Theme. |
fonts | [String.t()] | [] | Paths to font files to load. Loaded fonts are available by family name in any widget's font: prop. |
default_event_rate | integer() | unlimited | Maximum events per second for coalescable event types (mouse moves, scroll, slider drags, animation frames). Set to 60 for most apps, lower for remote rendering. Per-subscription max_rate and per-widget event_rate override this. |
Widget discovery
Native widgets are auto-detected via Plushie.Widget.WidgetProtocol
protocol consolidation at compile time. Any module that implements the
protocol and exports native_crate/0 is included in mix plushie.build
automatically.
Plushie.WidgetRegistry exposes the discovered widgets. No
configuration is needed. Add a native widget to your dependencies
and it appears in the next build.
Pure Elixir widgets (those with view/2 or view/3 callbacks) do not
require protocol consolidation or build system integration. They work
with any renderer binary, including precompiled downloads.
Transport modes
The renderer communicates with the SDK over a language-agnostic wire protocol (MessagePack or JSON over a byte stream). The SDK supports three transport modes:
:spawn (default)
The SDK spawns the renderer as a child process via an
Erlang Port.
Communication happens over the process's stdin/stdout. This is the
default for local development. mix plushie.gui uses this mode.
The Bridge manages the Port lifecycle: starting, monitoring, and
restarting the renderer on crash (with exponential backoff). The
renderer process inherits RUST_LOG from the environment.
:stdio
The renderer spawns the Elixir app (via plushie --exec "mix ...").
Communication happens over the BEAM's own stdin/stdout. The Elixir app
reads events from stdin and writes snapshots/patches to stdout.
This mode is used by the renderer's --exec flag, which starts the
renderer first, then launches the Elixir app as a subprocess. The
:binary option is ignored since no subprocess is spawned from the
Elixir side.
### {:iostream, pid}
A message-based adapter for custom transports. The pid is a process
that bridges between the wire protocol and an arbitrary transport (SSH
channel, TCP socket, Unix socket, WebSocket, etc.).
The adapter process must handle these messages:
| Direction | Message | Description |
|---|---|---|
| Bridge -> Adapter | {:iostream_bridge, bridge_pid} | Sent during init. The adapter stores the bridge pid for sending data back. |
| Bridge -> Adapter | {:iostream_send, iodata} | Protocol data to write to the transport. |
| Adapter -> Bridge | {:iostream_data, binary} | One complete protocol message received from the transport. |
| Adapter -> Bridge | {:iostream_closed, reason} | Transport closed. The bridge handles shutdown. |
The adapter is responsible for framing, splitting the byte stream
into complete protocol messages before sending {:iostream_data, ...}.
Plushie.Transport.Framing provides frame encode/decode for
length-prefixed byte streams (the standard wire framing).
This mode enables remote rendering: the Elixir app runs on one machine (server, embedded device, cloud), the renderer runs on another (desktop, laptop, mobile). The wire protocol is identical; only the transport layer differs. See the Shared State guide for a complete SSH example.
Test configuration
Plushie.Test.setup!/1 accepts options for the test session pool:
| Option | Type | Default | Purpose |
|---|---|---|---|
:pool_name | atom() | Plushie.TestPool | Registration name for the session pool process. |
:max_sessions | integer() | max(schedulers * 8, 128) | Maximum concurrent test sessions. Higher values allow more parallel tests but use more renderer memory. |
The test backend is selected via the PLUSHIE_TEST_BACKEND environment
variable or config :plushie, :test_backend. See the
Testing reference for backend setup and CI configuration.
See also
- Mix Tasks reference - binary resolution, build options, and all task flags
- Wire Protocol reference - message formats and framing details
- Testing reference - test backends and session pool
Plushie.Binary- binary resolution logic and versionPlushie.Bridge- transport management and restart behaviourPlushie.WidgetRegistry- widget discovery at compile timePlushie.App- the full callback specification includingsettings/0