Tibi

Plugins

Tibi's core ships the compiler and a thin runtime — every cross-cutting integration (Tailwind, MDX, Strands Auth, …) lives in the marketplace. Two install paths cover both reusable plugins and the bespoke ones you'd never publish.
  • Marketplace install — tibi plugin add <slug> fetches a curated manifest, patches your .tibi-config's plugins = […] array, and prints the cargo / file steps needed to wire the plugin in. Best for things other apps will reuse.
  • Project-local plugins — drop a file under plugins/ in JS, TypeScript, Python, or Rust. The server fires lifecycle events into it as a subprocess. Best for app-specific work (Storyblok sync, internal API webhook, one-off cache warmer).

The CLI commands cover both: tibi plugin search, tibi plugin show <slug>, tibi plugin add <slug>, tibi plugin list, tibi plugin remove <slug>. Slugs are [a-z0-9][a-z0-9-]{'{'}0,63{'}'}; anything else is rejected up-front. Override the marketplace source with TIBI_MARKETPLACE_URL — must be an http(s):// endpoint serving the same JSON shape. Local file:// / relative-path forms were removed in v0.2.13; serve mirrors over loopback HTTP instead.

Compiled Rust plugins land in dist/plugins-bin/<name>-<sha> (or .tibi-cache/plugins/ for the on-demand path) with a paired .sha256 sidecar carrying the binary's SHA-256 digest. The loader refuses to exec any binary whose sidecar is missing or whose hash mismatches, and rejects symlinks outright — closes a TOCTOU + cache-poisoning gap from v0.2.12.

Project-local plugins

For app-specific hooks, you skip the marketplace and just drop a file. Three languages, one contract. Pick the one your team writes most:

  • JS / TS — run via node. Best for plugins that talk to npm-ecosystem APIs (Storyblok SDK, Algolia, etc.).
  • Python — run via python3. Best when your team already lives in Python (data tooling, ML pipelines, internal SDKs).
  • Rust — compiled one-shot via rustc -O. Best for CPU-bound work or when you want type safety end-to-end.

Layout

my-app/
├── .tibi-config
└── plugins/
    ├── storyblok-sync.js
    ├── search-index.py
    └── cache-warmer.rs

The directory defaults to plugins/; override via [server] plugins_dir = "..." in .tibi-config. Missing dir = no plugins loaded (the directory is opt-in). Each file's stem becomes its plugin name in logs and MCP introspection: storyblok-sync.js → storyblok-sync.

The contract

When a lifecycle event fires, Tibi spawns the plugin's interpreter with the file as its only positional argument and four env vars set:

  • TIBI_EVENT — name of the event (on_build today; more land per phase).
  • TIBI_CTX — JSON blob with build context: {"event":"...","plugin":"<name>","plugins_dir":"<abs>"}. Grows per event; read only the keys you care about.
  • TIBI_PLUGIN_FILE — absolute path to the plugin source.
  • TIBI_PLUGINS_DIR — absolute path to the plugins directory.

Stdout is logged line-by-line with a tibi-plugin[<name>]: prefix. Non-zero exit shows on stderr. The runtime doesn't parse anything you print — the contract is void. If your plugin needs to push data into Tibi at runtime, hit the HTTP / WebSocket / MCP surface the same way any other client would.

Events

EventWhen
on_buildFires once at tibi serve boot (after the file-based router is loaded and channels / crons are spawned) and once at tibi build start. Use for cache warmers, index emitters, external content sync.

More events land alongside the phases: on_render (per-page server render), on_channel (channel tick), on_store_mutation (server-mirrored store writes). The env-var contract stays the same; only TIBI_EVENT and the TIBI_CTX JSON shape grow.

Examples

JavaScript / TypeScript

// plugins/hello.js const ctx = JSON.parse(process.env.TIBI_CTX); console.log(`hello from ${ctx.plugin} (event: ${ctx.event})`);

Python

#!/usr/bin/env python3 # plugins/hello.py import json, os ctx = json.loads(os.environ["TIBI_CTX"]) print(f"hello from {ctx['plugin']} (event: {ctx['event']})")

Rust

// plugins/hello.rs fn main() { let ctx = std::env::var("TIBI_CTX").unwrap_or_default(); let event = std::env::var("TIBI_EVENT").unwrap_or_default(); println!("hello from rust plugin (event: {event}, ctx: {ctx})"); }

Rust plugins are compiled with rustc -O. Keep them stdlib-only — if you need crates, write a separate cargo project and call its binary from a JS / Python wrapper plugin.

How Rust plugins find their binary

Tibi never recompiles if it already has a binary for this source. Each plugin's source is hashed (FNV-1a, 8 hex chars) and the loader searches in order:

  1. <project>/dist/plugins-bin/<name>-<sha8> — emitted by tibi build (or tibi plugin compile). Ship dist/ with your deploy and Rust plugins run on hosts without rustc.
  2. <project>/.tibi-cache/plugins/<name>-<sha8> — boot-time cache. Populated on first run when rustc is on PATH. Survives restarts; safe to delete.
  3. Compile via rustc into the cache and run.
  4. None of the above → the server logs a one-line skip pointing at tibi build and keeps serving. JS / Py plugins keep running.

OS / arch caveat: a binary built on dev linux won't run on macOS or a different arch. Build on the deploy target if you ship pre-compiled binaries (same caveat as any AOT-compiled plugin). The cache hash is content-only, not target-aware — if you swap architectures, wipe .tibi-cache/.

Deploy recipes

  • Docker — run tibi plugin compile in your build stage, COPY dist/plugins-bin /app/dist/plugins-bin into the runtime image. No rustc needed at runtime.
  • Netlify / Vercel / Cloudflare Pages — add tibi plugin compile to your build command. The platform's static asset pickup includes dist/.
  • SSR on bare metal / VM — easiest path: install rustc on the box once, the loader auto-caches. Or run tibi build on a matching-arch CI runner and rsync the result.

Introspection

The bundled MCP server exposes tibi_plugins: lists every plugin Tibi has loaded for the configured project as <name> <lang> <file>. Same shape as tibi_routes / tibi_channels / tibi_crons — use it from your agent setup to confirm the right plugin set is shipping in any given deploy.

Packaged plugins (component bundles)

For plugins that ship .tibi components rather than a build hook, Tibi uses a two-file packaging format: tibi-plugin.toml (manifest with packer-filled [bundle] sha256 + size) paired with tibi-plugin.tpkg (deterministic gzipped tar). Drop the pair under plugins/<slug>/; the runtime verifies the digest at boot, extracts to .tibi-cache/plugin-packages/<name>-<sha16>/, then vendors every component into .tibi-cache/components-vendored/<Prefix+Tag>.tibi so the compiler's resolver picks them up alongside your own.

First-party packages currently shipped: icons, prose, ui, code, toast, link. Each uses prefix = "Ti", so a component named Button.tibi resolves as <TiButton/>. Live examples inline:

<TiButton variant="primary">, <TiBadge tone="success">, <TiSpinner/> all rendered from the ui package:

Primary Ghost brand success

The code package ships <TiCode lang="…"> with an inline copy button:

tibi plugin pack ./my-widgets

Author your own: tibi plugin new <name> scaffolds the layout (manifest + components/Sample.tibi) under <cwd>/<name>/. Omit <name> to be prompted; pass --prefix <Xy> to change the tag namespace (default Ti), --out <dir> to redirect the parent folder, --no-sample to skip the placeholder component. When the sources are ready, tibi plugin pack <src> writes the paired tibi-plugin.toml + tibi-plugin.tpkg under dist/plugins/<name>/. Copy that pair into a target project's plugins/<name>/ to install it. Same source bytes always produce the same bundle bytes, so a re-pack lets you spot tampering after the fact.

There is no central submission flow yet -- sharing packaged plugins is manual (commit the bundle to a shared repo, distribute via your own download, etc.). A public submission page lands later.

Browser injection ([assets])

Packaged plugins can also ship browser-side code. Three manifest keys under [assets] cover it: inject_js (a JS file concatenated into the runtime bundle, after the framework modules and the project prelude, before any component IIFE runs), inject_css (concatenated after global_css), and config_env (a list of env var names snapshotted at boot/build time and surfaced to the browser as window.__tibiPluginConfig.<plugin>.<NAME>; missing vars come through as empty strings). The first-party tether plugin is the reference example: its client.js reads the config, exposes window.Tibi.tether, and never touches the compiler.

As of 1.0.60 the same assets ship in both render paths: tibi serve injects them per request, and tibi build bakes them (plus the __tibiPluginConfig / __tibiStoreConfig prelude, snapshotted from the build environment) into the static dist/__tibi/runtime.<sha>.js bundle. Statically deployed sites get the same plugin surface an SSR deploy would.

Reactive helpers via the scope hook

Injected plugin JS runs once per page, outside any component, so it normally can't reach the per-component reactive primitives (state / effect / onUnmount only exist inside a <script tibi> body). The scope hook closes that gap: while a component (or page) <script tibi> body is initialising, the framework parks that component's scope bag on window.Tibi.currentScope(). A plugin helper called from the init body can grab it, mint state cells against the calling component, and register its teardown with the component's onUnmount:

// inside a plugin's inject_js function useTicker(ms) { const scope = window.Tibi.currentScope('useTicker'); // throws a helpful error outside init const count = scope.state(0); const timer = setInterval(() => { count.value++; }, ms); scope.onUnmount(() => { clearInterval(timer); }); return count; }

The bag also exposes host (the component's element), computed, onMount, on, and emit. Nesting is handled: a child component connecting mid-init saves and restores the parent's scope, so helpers always bind to the component whose body is actually running. The tether plugin builds its useQuery / useMutation / useSubscription helpers on exactly this hook.

Writing reusable plugins

For something you'd reuse across multiple apps — a Tower middleware layer, a CSS preprocessor, a new directive — write it against the TibiPlugin trait in tibi-plugin. Marketplace plugins can ship as either crate (Rust-native, integrated into the compiler / runtime) or script (a JS / Py / Rust file dropped into the user's plugins/). The CLI dispatches to the right install path automatically based on the manifest's kind.

The marketplace is curated by the Tibi team — community submissions aren't open yet.