Skip to content

Node

Build, supervise, and back up Chickpea as a single Node service on a host you own.

Updated View as Markdown

A Node deployment is one supervised Chickpea process on a host you own, with its state in SQLite files beside it. Where the Cloudflare target spreads state across Durable Objects and D1 and adds schedules and the coding sandbox, Node keeps everything on one host and has neither. This page covers building a release, the runtime environment file and state paths, the systemd unit, HTTPS, backups and restore, and the limits of the target.

Prerequisites

  • Node 24.x, minimum 24.20.0. package.json requires >=24.20.0 <25 and .nvmrc pins 24.20.0. Run nvm install && nvm use for the reproducible baseline. Later Node 24 updates are supported; other majors are outside the support policy.
  • One process per state directory. Do not run several processes against the same SQLite files, and do not use a network filesystem as a substitute for shared-state support.
  • Your own Slack app. Pick Use your own Slack app during setup. The Node target does not advertise durable event admission to the shared gateway, so Add to Slack is not a reliable ingress lane here.
  • A reverse proxy that terminates HTTPS. The production entry point exposes PORT and no host setting.

One deployment currently serves one Slack workspace.

Build a release

Check out the release tag you intend to run, install its exact lockfile, and build the production entry point.

git checkout <release-tag>
npm ci
npm run flue:build

The build writes dist/, and the entry point is dist/server.mjs. That is not the Vite development server npm run dev starts, which is for development only.

Keep the release checkout, its node_modules, migrations/, and assets/ available at runtime. Chickpea resolves its auth migrations at migrations/better-auth relative to the process working directory, so the service must run from the checkout. The built server reads runtime environment variables and does not load a development .env file.

Persist state and secrets

Create a dedicated OS account, a private state directory such as /var/lib/chickpea, and a protected runtime environment file such as /etc/chickpea/runtime.env. Use mode 0700 for the directory and 0600 for the environment file, both owned by that account. Do not put state inside a checkout that an upgrade will replace.

/etc/chickpea/runtime.envdotenv
NODE_ENV=production
PORT=3000
TAG_DB_PATH=/var/lib/chickpea/transcripts.sqlite
SLACK_STATE_DB_PATH=/var/lib/chickpea/state.sqlite
CHICKPEA_AUTH_DB_PATH=/var/lib/chickpea/auth.sqlite
CHICKPEA_CREDENTIAL_KEYRING_PATH=/var/lib/chickpea/credential-keyring.json
CHICKPEA_AUTH_SECRET=<32 random bytes, generated once>
SLACK_TAG_PUBLIC_URL=https://chickpea.example.com

Generate the auth secret once and never regenerate it on start:

openssl rand -base64 32 | tr '+/' '-_' | tr -d '='

For first-time setup, run npm run setup:link -- https://chickpea.example.com and add the printed CHICKPEA_SETUP_CAPABILITY_DIGEST and CHICKPEA_SETUP_CAPABILITY_ISSUED_AT to the same file. Keep the private setup link out of logs and source control.

The credential keyring is an independent 256-bit root and is never derived from CHICKPEA_AUTH_SECRET. Chickpea creates it on first use with mode 0600, inside a directory it creates with mode 0700. If its group or other permission bits are set, startup fails with Node Slack credential keyring permissions are unsafe at <path>; run chmod 600 and retry. Losing encryption keys makes stored credentials unusable.

Setting the four paths explicitly avoids these development defaults, which are derived from the resolved state path when no override is set:

Data Default path
Conversation transcripts ./tmp/flue.db
App state, identity, claims, configuration <TAG_DB_PATH>.state
Sign-in sessions and accounts <SLACK_STATE_DB_PATH>.auth
Slack credential encryption keys <SLACK_STATE_DB_PATH>.credential-keyring.json

Never use :memory: for a production database. Product telemetry labels a Node deployment development unless you set CHICKPEA_TELEMETRY_ENVIRONMENT=production.

Run under a supervisor

Adapt these paths to your host. /usr/bin/node must be the supported Node binary, and /opt/chickpea/current must point at the release you built.

/etc/systemd/system/chickpea.serviceini
[Unit]
Description=Chickpea Slack agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=chickpea
Group=chickpea
WorkingDirectory=/opt/chickpea/current
EnvironmentFile=/etc/chickpea/runtime.env
ExecStart=/usr/bin/node dist/server.mjs
Restart=always
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=75
UMask=0077
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/chickpea

[Install]
WantedBy=multi-user.target

The entry point handles SIGTERM, waits for shutdown, and holds a 60-second internal deadline before it exits anyway. TimeoutStopSec=75 leaves that deadline room to finish, so systemd does not send SIGKILL first.

Put HTTPS in front

Terminate HTTPS at a reverse proxy and forward to port 3000. The entry point reads PORT and exposes no HOST setting, so use firewall or container network rules to make the Node port unreachable from the public internet.

Preserve the public host and the HTTPS scheme through the proxy, avoid request-body logging, and configure streaming rather than buffering for Slack-related responses. Complete Slack setup using the exact HTTPS origin you set in SLACK_TAG_PUBLIC_URL.

Verify

Before routing normal traffic, confirm all three:

  • Sign-in works. Slack OIDC is the only human sign-in. No passwords.
  • A real Slack reply arrives in a thread from an Agent.
  • State survives a service restart. Restart the unit, then check that the same Agents, connections, and thread history are still there.

Back up and restore

Stop the service cleanly

Schedule an interruption, pause work where supported, and stop new ingress. Let active work finish, then stop the Chickpea service.

Snapshot the whole state directory while stopped

Include any SQLite -wal and -shm files and the credential keyring. Chickpea opens its state databases in WAL journal mode, so copying only the main SQLite files while the service is running is not a consistent backup. Back up the runtime environment file and record the exact release commit separately.

Encrypt and restrict the backup

Set a retention policy appropriate for the Slack content and credentials the snapshot contains. Do not attach backups to issues.

Test the restore on an isolated host

Disable outbound traffic there. Restore every database and key from the same snapshot, with the same auth secret and the same code version, and confirm SQLite integrity before allowing any Slack or provider traffic. A backup is not proven until its restore has been tested.

Limits

Behaviour On Cloudflare On Node
Schedules A one-minute cron runs due schedules Not supported. Creating, editing, resuming, or cloning a schedule is refused, and Slack answers Scheduling is currently Cloudflare-only. Inspecting, pausing, and deleting existing ones still work
Coding sandbox Optional container tier Always the standard sandbox, which never touches the host filesystem or host git and SSH
Slack ingress Both lanes; the shared gateway lane admits the event durably before acknowledging it Use your own Slack app; the shared gateway lane has no durable-admission contract on Node
State Durable Objects and D1 AUTH_DB SQLite files in one state directory, one process at a time

Next steps

  • Deploy and operate: the Cloudflare target, upgrades, and configuration.
  • Get started on Node: the first-run path on Node, if this deployment does not exist yet.
  • Security: the security model, the shared Slack gateway, telemetry, and recovery.
  • Environment variables: every environment variable, with when it is required and what it defaults to.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close