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
| Node | 20 or newer (engines in apps/agents/package.json); the image builds on the current LTS |
| pnpm | 9.15.4 is the pinned packageManager; a newer pnpm works with --node-linker=isolated |
| PostgreSQL | 16 is what NuFi runs |
| Kubernetes + Cilium | Required 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:mainWithout 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:serverWork 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.mjs5 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=readyThen two settings, in two places. Both are required — the flow silently does nothing useful with only one.
| Where | Setting | Value |
|---|---|---|
| Agents → Settings → Plugins → NuFi Connection | NuFi console URL | https://console.nufi.me |
| Console environment | AGENTS_ALLOWED_ORIGINS | https://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> --squashThen 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.