Reshape the UI, no redeploy.
@faraday-stack/forge is a React SDK that lets your users describe interface changes — hide a column,
rename a label, recolor a header — and an agent applies them live, scoped to the elements you opt in.
Engineers wrap parts of the tree with <Modifiable> or the useModifiable hook. Users open a floating chat widget, describe what they want changed, and the agent edits the interface while
its response streams in. All requests route through the FaradayStack backend. We take care of all the agent infrastructure
yourself.
01Installation
Install the package and import the stylesheet once at your app's root.
# add to your project pnpm add @faraday-stack/forge # or with npm / yarn / bun npm install @faraday-stack/forge
Peer dependencies: react ≥18, react-dom ≥18. The SDK is tree-shakable and ships ESM +
CJS with full type defs.
02Quick start
Wrap your app in a provider, then mark elements as modifiable. Anything not marked is invisible to the agent.
import { UIAgentProvider, Modifiable, useModifiable, } from "@faraday-stack/forge"; import "@faraday-stack/forge/style.css"; function App() { return ( <UIAgentProvider publishableKey={import.meta.env.VITE_FARADAY_PUBLISHABLE_KEY} userToken={currentUser.faradayToken} permissions={{ allowedStyleProps: ["color", "background", "fontSize", "padding", "borderRadius"], persist: "user", }} > <Hero /> </UIAgentProvider> ); } function Hero() { const { text, style, visible } = useModifiable("hero-title", { text: "Welcome", }); if (!visible) return null; return <h1 style={style}>{text}</h1>; }
userToken is valid. Element selection
is opt-in — only IDs you register show up in the agent's page snapshot.03Authenticating the provider
The provider takes two credentials: a publishable key that identifies your project, and a user token minted on your backend per logged-in user.
| Prop | Source | Notes |
|---|---|---|
| publishableKey● | Dashboard → API keys | Safe to ship in client bundles. Identifies your project, not your users. |
| userToken● | Your backend, per session | Short-lived JWT scoped to one user. Mint server-side and pass to React. |
| permissions | — | CSS allow-list, undo depth, and persistence mode. |
| components | — | Registry of components the agent can insert into containers. |
| onAction | — | Callback fired after every applied agent action. |
publishableKey is missing, or
if it's set without a userToken.Getting a publishable key
- Sign in at app.faraday.dev.
- Create or select a project.
- Open API keys and copy the value prefixed with
pk_live_(production) orpk_test_(test mode). - Put it in an env var your bundler exposes to the client — e.g.
VITE_FARADAY_PUBLISHABLE_KEYorNEXT_PUBLIC_FARADAY_PUBLISHABLE_KEY.
Minting user tokens
User tokens are short-lived JWTs you generate on your backend using your secret key (kept server-side, never shipped). A typical Express handler:
import { mintUserToken } from "@faraday/server"; app.get("/api/faraday/token", requireAuth, async (req, res) => { const token = await mintUserToken({ secretKey: process.env.FARADAY_SECRET_KEY!, userId: req.user.id, // optional: claims surfaced to the agent claims: { orgId: req.user.orgId, role: req.user.role }, }); res.json({ token }); });
Fetch the token after login and feed it to the provider. Refresh on a timer or when the SDK reports an expired
token via onAction.
04UIAgentProvider
The root of every FaradayStack-enabled tree. Wires up the agent runtime, opens a websocket to the FaradayStack backend, mounts the chat widget, and exposes the override store via context to all descendants.
Props
pk_live_… or pk_test_…). Safe to ship in
client bundles. Throws at mount if missing.allowedStyleProps: string[] — CSS properties applyStyle may write. Default: [] (style edits disabled).maxUndoDepth: number — actions retained for the undo tool. Default: 50.persist: "none" | "session" | "user" — where overrides live. Default: "none".insertComponent. { component: ComponentType, propsSchema: Record<string, string> }. Schema
strings use a pipe-union syntax: "info|warn|error"."token_expired" errors and refresh the JWT.console.warn."inline" renders nothing — you mount <AgentChat /> yourself. false disables the widget entirely (programmatic-only).Returns
A JSX element that renders children wrapped in two contexts: OverrideStoreContext (read by every Modifiable / useModifiable descendant) and AgentRuntimeContext (the websocket + tool dispatcher). When widget is enabled, also renders
the floating chat surface as a sibling.
05<Modifiable> component
The declarative way to mark an element as agent-reachable. Use this when the element is a plain leaf — a heading, paragraph, image — and you don't need the override values in your own render logic.
Props
"h1", "aside"); component
refs render that component with overrides spread as props."container" opts the element into insertComponent and reorder tools. Leaves
only accept text/style/visibility edits.false hides it until setVisibility reveals it.defaultText is set.Returns
A JSX element of type as, or null when the merged visibility is false.
The element receives merged style, current text content, and a data-faraday-id attribute used by the
runtime to scroll the agent's focus pill into view.
06useModifiable hook
The imperative form of <Modifiable>. Call it inside a component to subscribe to the agent's
overrides for a given id, then apply the returned values yourself. Use this when you need the values in your own
render logic — conditional rendering, animation, prop forwarding, etc.
Parameters
<Modifiable> in the page snapshot. The hook registers
the id on mount and unregisters on unmount.text?: string — fallback text content.style?: React.CSSProperties — baseline style merged behind agent overrides.visible?: boolean — initial visibility. Default: true.label?: string — agent-facing hint.Return value — ModifiableState
defaults.text until setText overrides it. Render
this directly in your JSX so streaming agent edits appear live.defaults.style overlaid with agent applyStyle results,
filtered through allowedStyleProps. Spread it onto the host element's style prop.setVisibility tool. Convention: early-return null when this is false so the element drops out of the tree (and out of the agent's
snapshot).defaults. Useful for per-element
"reset to original" affordances.<Modifiable> and useModifiable register the element in the agent's page snapshot
on mount and unregister on unmount. Unmounted ids are silently dropped from any in-flight tool call.Combined example
function Hero() { const { text, style, visible, attrs, override, reset } = useModifiable("hero-title", { text: "Welcome", style: { fontSize: 48 }, }); if (!visible) return null; return ( <header> <h1 {...attrs} style={style}>{text}</h1> {override && <button onClick={reset}>Revert</button>} </header> ); }
07Insertable components
If you want the agent to insert new components — not just restyle existing ones — register them in the components map and mark a container.
<UIAgentProvider publishableKey={import.meta.env.VITE_FARADAY_PUBLISHABLE_KEY} userToken={session.faradayToken} components={{ Card: { component: Card, propsSchema: { title: "string", body: "string" } }, Alert: { component: Alert, propsSchema: { variant: "info|warn|error", message: "string" } }, }} > <Modifiable id="sidebar" as="aside" type="container" /> </UIAgentProvider>
08Available agent tools
The agent has six built-in tools, dispatched on every prompt. applyStyle only passes CSS keys
listed in allowedStyleProps — everything else is dropped before it reaches the store.
allowedStyleProps.visible:false.maxUndoDepth.09Development
From the package/ directory:
pnpm build # build to dist/ pnpm dev # watch mode pnpm test # run tests (jsdom) pnpm typecheck # tsc --noEmit pnpm lint # eslint src pnpm lint:publish # publint — validates published metadata pnpm check:exports # arethetypeswrong — validates dist resolution
10Releasing
Releases are driven by Changesets. When you make a user-visible change to this package:
pnpm changeset
Pick patch, minor, or major and write a one-line consumer-facing summary.
Commit the generated .changeset/*.md file with your PR.
main, the release workflow opens (or updates) a Version Packages PR that bumps package.json and rewrites CHANGELOG.md. Merging that PR publishes to npm with provenance and creates a GitHub Release. No manual npm publish needed.