← home/docs/@faraday-stack/forge/Overview

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.

npm · v0.1.0 react ≥18 react-dom ≥18 node ≥18 MIT ~24 kB gz
install
> pnpm add @faraday-stack/forge

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.

bash copy
# 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.

app.tsx copy
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>;
}
>
Once mounted, the floating chat widget is enabled for any user whose 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.

PropSourceNotes
publishableKeyDashboard → API keysSafe to ship in client bundles. Identifies your project, not your users.
userTokenYour backend, per sessionShort-lived JWT scoped to one user. Mint server-side and pass to React.
permissionsCSS allow-list, undo depth, and persistence mode.
componentsRegistry of components the agent can insert into containers.
onActionCallback fired after every applied agent action.
!
Both are required. The provider throws at mount if publishableKey is missing, or if it's set without a userToken.

Getting a publishable key

  1. Sign in at app.faraday.dev.
  2. Create or select a project.
  3. Open API keys and copy the value prefixed with pk_live_ (production) or pk_test_ (test mode).
  4. Put it in an env var your bundler exposes to the client — e.g. VITE_FARADAY_PUBLISHABLE_KEY or NEXT_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:

server / api / faraday / token.ts copy
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.

// signature function UIAgentProvider(props: UIAgentProviderProps): JSX.Element

Props

Prop · type · default
Description
publishableKey required
string
Your project's public-side identifier (pk_live_… or pk_test_…). Safe to ship in client bundles. Throws at mount if missing.
userToken required
string
Short-lived JWT minted by your backend, scoped to one user. The agent runtime uses it to authenticate every action and to key persisted overrides. Refresh on expiry.
children required
React.ReactNode
The subtree that should be reachable by the agent. Anything outside the provider is invisible to it.
permissions optional
PermissionsConfig
default: see below
What the agent is allowed to do.
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".
components optional
Record<string, ComponentEntry>
default: {}
Registry of components the agent can insert into containers via insertComponent.
Each entry: { component: ComponentType, propsSchema: Record<string, string> }. Schema strings use a pipe-union syntax: "info|warn|error".
onAction optional
(action: AgentAction) => void
Fires synchronously after every applied agent action. Use it for analytics, audit logs, or to detect "token_expired" errors and refresh the JWT.
onError optional
(err: AgentError) => void
Catches non-fatal runtime errors (failed action, dropped websocket, schema mismatch). If unset, errors log to console.warn.
widget optional
"floating" | "inline" | false
default: "floating"
Controls how the chat surface mounts. "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.

// signature function Modifiable(props: ModifiableProps): JSX.Element | null

Props

Prop · type · default
Description
id required
string
Stable identifier the agent uses to address the element. Must be unique within the page snapshot. Reusing an id across mounts re-binds to the same override record.
as optional
keyof JSX.IntrinsicElements | ComponentType
default: "div"
The host element to render. Strings map to native tags ("h1", "aside"); component refs render that component with overrides spread as props.
type optional
"leaf" | "container"
default: "leaf"
"container" opts the element into insertComponent and reorder tools. Leaves only accept text/style/visibility edits.
defaultText optional
string
Initial text content rendered until the agent (or a persisted override) replaces it.
defaultStyle optional
React.CSSProperties
Baseline style merged behind any agent overrides. Agent-set keys win on conflict.
defaultVisible optional
boolean
default: true
Whether the element renders before the agent decides otherwise. false hides it until setVisibility reveals it.
label optional
string
Human-readable hint surfaced to the agent in the page snapshot. Improves targeting accuracy when ids are opaque (e.g. UUIDs).
children optional
React.ReactNode
For containers, the static children rendered around any agent-inserted components. Ignored on leaves when 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.

// signature function useModifiable( id: string, defaults?: ModifiableDefaults): ModifiableState

Parameters

Param · type · default
Description
id required
string
Stable identifier shared with any matching <Modifiable> in the page snapshot. The hook registers the id on mount and unregisters on unmount.
defaults optional
ModifiableDefaults
default: {}
Initial values returned until the agent (or persistence layer) provides an override.
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

Field · type
Functionality
text
string
The current text content. Equal to defaults.text until setText overrides it. Render this directly in your JSX so streaming agent edits appear live.
style
React.CSSProperties
Merged style object: defaults.style overlaid with agent applyStyle results, filtered through allowedStyleProps. Spread it onto the host element's style prop.
visible
boolean
Current visibility. Toggled by the 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).
attrs
{ "data-faraday-id": string }
Spread onto your host element so the runtime can locate it for the focus pill, scroll-into-view, and outline-on-hover behaviors. Required for visual feedback during streaming edits.
override
Override | undefined
The raw override record (timestamp, author, action history) — useful for showing "edited by agent" badges, diffing against defaults, or building your own undo UI.
reset
() => void
Imperatively clears the override for this id, reverting to defaults. Useful for per-element "reset to original" affordances.
Both <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

hero.tsx copy
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.

tsx copy
<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.

applyStyle
Set CSS properties on a modifiable element. Filtered through allowedStyleProps.
setText
Change the text content of an element.
setVisibility
Show or hide an element. Hidden nodes return visible:false.
reorder
Reorder inserted components inside a marked container.
insertComponent
Insert a registered component into a container with typed props.
undo
Revert the last N actions, bounded by maxUndoDepth.

09Development

From the package/ directory:

bash copy
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:

bash copy
pnpm changeset

Pick patch, minor, or major and write a one-line consumer-facing summary. Commit the generated .changeset/*.md file with your PR.

After your PR merges to 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.