NuFiDocs

Installing NuFi Works

Requirements for NuFi Works, how to point it at the gateway, and how to check the install actually took.

NuFi Works is a Node server and a React UI on PostgreSQL. It is deployed separately from the chat app and shares only the gateway.

Requirements

Node20 or newer (engines in apps/agents/package.json); the image builds on the current LTS
pnpm9.15.4 is the pinned packageManager; a newer pnpm works with --node-linker=isolated
PostgreSQL16 is what NuFi runs
Kubernetes + CiliumRequired to enforce agent egress — see Agent egress

The gateway is a hard dependency: every model call goes through it.

Run the published image

The supported path is the image, ghcr.io/dudaji-vn/nufi-works:main (a nufi-works-vX.Y.Z tag on the repository publishes vX.Y.Z; none has been cut yet). It listens on 3100 and needs a Postgres, a volume at /paperclip for workspaces and uploads, and the adapters file that is already inside the image:

docker run -d --name nufi-works -p 3100:3100 -v nufi-works-data:/paperclip \
  -e PORT=3100 \
  -e DATABASE_URL=postgresql://user:pass@host:5432/nufi_works \
  -e BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
  -e PAPERCLIP_DEPLOYMENT_MODE=authenticated \
  -e PAPERCLIP_DEPLOYMENT_EXPOSURE=public \
  -e PAPERCLIP_PUBLIC_URL=https://works.example.com \
  -e PAPERCLIP_HOME=/paperclip \
  -e PAPERCLIP_ADAPTERS_FILE=/app/nufi/adapters.json \
  ghcr.io/dudaji-vn/nufi-works:main

Without the volume every redeploy starts empty. With PAPERCLIP_DEPLOYMENT_EXPOSURE=public the first-admin claim in the browser is disabled; claim the instance while it is still private, then switch. deploy/railway/agents.md is the record of NuFi's own instance, variable by variable, including the sign-in block from Single sign-on for the agent apps.

From source

For development, or for a host where you build the image yourself:

cd apps/agents
pnpm install --frozen-lockfile --node-linker=isolated
pnpm build
node nufi/rebrand-server-dist.mjs server/dist
pnpm db:migrate
pnpm dev:server

Work on NuFi Studio and NuFi Works walks through the same steps with the values that were used to check them. Two of those lines are easy to skip and both fail quietly.

--node-linker=isolated is not optional. The UI build aliases a dependency to a hardcoded path that only exists under pnpm's default isolated layout. A machine configured for hoisted linking — including via a global file such as ~/Library/Preferences/pnpm/rc — puts the package elsewhere and the build dies on an unrelated-looking ENOENT. Check with pnpm config get node-linker.

The rebrand step must run after every server build. The UI gets the product name from a build plugin; the server builds with plain tsc and gets it from this script. Skip it and the two halves disagree — the client looks for one string while the server writes another, and comparisons between them stop matching with no error at all. Assert it with node nufi/rebrand-server-dist.mjs --check server/dist, which exits non-zero if the step was missed.

Configuration

DATABASE_URL=postgres://…
PORT=3100
PAPERCLIP_ADAPTERS_FILE=/path/to/apps/agents/nufi/adapters.json
NUFI_MODEL_API_KEY=<gateway key>

PAPERCLIP_ADAPTERS_FILE is what pins agents to the gateway. It replaces the built-in adapter list wholesale, so an adapter absent from the file is unavailable rather than falling back to a vendor default.

NUFI_MODEL_API_KEY is now a fallback, not the only way in. It is one key shared by every agent on the server: spend is a single number, and revoking it revokes it for everyone. Prefer per-member keys via the connect plugin and leave this unset unless the install is genuinely single-tenant.

Verifying the install

Adapters resolved as intended. On startup the server logs which it accepted:

reconciled adapter availability from PAPERCLIP_ADAPTERS
  {"enabled":["claude_local","codex_local","opencode_local","pi_local","nufi_agent"],
   "disabled":["cursor","cursor_cloud","gemini_local","grok_local",…]}

If a type you expected is in disabled, it is missing from adapters.json. If the server refuses to start with "declares adapter type(s) with no installed adapter", the name in the file does not match what the adapter declares.

The registry is internally consistent:

node apps/agents/nufi/verify-adapters.mjs
5 adapters — 5 enabled, 0 disabled
OK — every enabled adapter routes and egresses only to api.codechi.me.

Egress is actually confined — the check that matters, and the one that is not satisfied by configuration alone. There is no script for Works yet; the by-hand test, from inside a running agent pod, is on Agent egress. Until it passes, agent traffic is routed to the gateway by convention, not held there by policy. Do not describe the deployment as protected before it does.

The connect plugin

Lets each member hand this installation their own gateway key from the session they already have at NuFi chat, so runs bill the person the work belongs to. End-user steps: Connecting your NuFi account.

cd apps/agents
pnpm --dir nufi/connect-plugin install
pnpm --dir nufi/connect-plugin build
pnpm paperclipai plugin install "$PWD/nufi/connect-plugin"
pnpm paperclipai plugin list      # expect: key=nufi.connect status=ready

Then two settings, in two places. Both are required — the flow silently does nothing useful with only one.

WhereSettingValue
Agents → Settings → Plugins → NuFi ConnectionNuFi console URLhttps://console.nufi.me
Console environmentAGENTS_ALLOWED_ORIGINShttps://works.nufi.me (the origin that opens the connect window), comma-separated with any other you run

AGENTS_ALLOWED_ORIGINS is a security control, not configuration ceremony. Any page can open the console's connect window, and a signed-in visitor is legitimately recognised there; this list is the only thing that stops the console handing the minted key back to whoever asked. Matching is exact — no wildcards, no suffix matching — and an empty value disables the endpoint rather than opening it.

Then, once per company, an administrator opens Settings → NuFi and adds the NUFI_MODEL_API_KEY secret. That creates the slot; each member still supplies their own value. Bind it on an agent as an environment variable of the same name referencing the user secret, and every run resolves it to the key of whoever the work belongs to.

Installing the NuFi adapter

nufi_agent is an external adapter, so it needs registering once:

[{
  "packageName": "@nufi/paperclip-adapter",
  "localPath": "<repo>/apps/agents/nufi/adapter",
  "type": "nufi_agent",
  "installedAt": "2026-08-04T08:00:00.000Z"
}]

Write that to ~/.paperclip/adapter-plugins.json, build the adapter (pnpm --dir nufi/adapter build), and restart. Success looks like:

Loaded external adapters from plugin store {"count":1,"adapters":["nufi_agent"]}

Upgrading

apps/agents tracks upstream release tags, not the development branch:

git fetch --depth 1 paperclip refs/tags/<tag>:refs/tags/paperclip-<tag>
git subtree pull --prefix=apps/agents paperclip-<tag> --squash

Then re-run the build, the rebrand step, and the migrations. The fork-guard job in agents-ci.yml rejects the pull request if the upgrade introduced local edits outside the NuFi-owned file list, which is what keeps this upgrade path working at all.