Hot swap
Change the variety without felling the tree. Hot reload swaps only the plugins your change affects, in one transaction, and keeps the old code if the new code fails. See hot reload.
Software is not built. It is grown.
Plugins arrive, take root and bear fruit. Some are pruned; some are replaced while the tree keeps standing. A good gardener never wonders where a branch came from, or what happens when it is cut.
Graft gives you that certainty. Every plugin is grafted onto a scope. Every fiber has an owner. Every failure is traced back to its root.
Hot swap
Change the variety without felling the tree. Hot reload swaps only the plugins your change affects, in one transaction, and keeps the old code if the new code fails. See hot reload.
Clean teardown
Each plugin runs in its own Scope. When it goes away, its
dependents stop first, then its finalizers run. A failure keeps its full
Cause. See lifecycle.
Checked dependencies
Services are Effect Context keys, provided by layers or by
other plugins. A plugin lists what it needs in inject, and the
compiler checks the list. See services.
import { Context, Effect, Option, Schema, Stream } from "effect"
import { Fork, Graft, Plugin } from "@graftland/graft"
class Greeter extends Context.Service< Greeter, { readonly greet: (name: string) => Effect.Effect<void> }>()("app/Greeter") {}
// Publishes a Greeter while it is loaded.const greeter = Plugin.make({ name: "app/greeter", config: Schema.Struct({ greeting: Schema.String }), apply: ({ greeting }) => Graft.provide(Greeter, { greet: (name) => Effect.log(`${greeting}, ${name}!`), }),})
// Starts once a Greeter exists, and stops before it goes away.const hello = Plugin.make({ name: "app/hello", inject: [Greeter], apply: () => Effect.flatMap(Greeter, (greeter) => greeter.greet("world")),})
const states = await Effect.runPromise( Effect.gen(function* () { const helloFork = yield* Graft.plugin(hello) yield* helloFork.await const waiting = yield* Stream.runHead(helloFork.state)
yield* Graft.plugin(greeter, { greeting: "Hello" }) const started = yield* helloFork.state.pipe( Stream.filter( (state) => state._tag !== "Pending" && state._tag !== "Loading", ), Stream.runHead, ) return [waiting, started] }).pipe(Effect.scoped, Effect.provide(Graft.layer)),)states // => [Option.some(Fork.ForkState.Pending({ missing: ["app/Greeter"] })), Option.some(Fork.ForkState.Active())]Get started
Create a project and run it with hot reload in the quickstart. Then read the core concepts.
Write plugins
Run an application
Describe it in a config file, start it with the command line, and edit it with hot reload.
Run plugins elsewhere
Move one plugin into a child process, a worker or another machine with Remote. The plugins that use it do not change.
Coming from cordis?
| Section | Pages | Read it to… |
|---|---|---|
| Start here | Quickstart, Core concepts, Coming from cordis | Learn the model and run something. |
| Build plugins | Plugins, Services, Events | Write the code. |
| Structure | Lifecycle, Realms, Intercepts, Replace plugins | Control how plugins relate and change. |
| Applications | Config file, Command line, Hot reload, Use Graft in your program | Run and operate an app. |
| Remote | Overview, Host entries, Runners, Replacement and stopping | Run a plugin in another process or on another machine. |
| Reference | Fork handle, Loader API, HMR API, Errors | Look up exact behavior. |
| Project | Releases | Check and publish Graft. |
| Package | What it does |
|---|---|
@graftland/graft |
A plugin framework built on Effect: plugins, forks, services, isolation, events and logs. |
@graftland/loader |
Loads plugins from a list of entries or a YAML config file. |
@graftland/hmr |
Reloads plugin modules on Node.js without restarting the process. |
@graftland/cli |
The graft command: start, create, add and remove. |
@graftland/create |
Makes a new Graft project: npm create @graftland and graft create. |
@graftland/package-manager |
Installs dependencies with npm, pnpm or Yarn. A failed install is undone by snapshot rollback. |
@graftland/plugin-remote |
Runs a plugin in a child process, a worker or on another machine. See Remote. |
@graftland/plugin-otlp |
Exports the spans of a subtree of plugins to OpenTelemetry. See Export spans. |
@graftland/plugin-http |
Shared HTTP server; routes and streams follow plugin lifetimes. |
@graftland/plugin-http-api |
Live HttpApi groups, typed clients and OpenAPI documentation. |
@graftland/plugin-mcp-server |
Serves the tools of plugins to MCP clients. See MCP server. |
All packages share one version number and are released together. They need
effect 4.1.x.
The garden is only on this page. In the rest of the documentation and in the
code, a Scope is still a Scope. Graft draws its plugin model from
cordis. Where cordis composes across
space and time, Graft keeps to the garden.