Skip to content

Releases and deploy ​

Versions ​

Only @wireface/chat is published to npm. Everything else is private ("private": true) and ships in the Docker image. The npm package is versioned with Changesets, set up in .changeset/:

sh
pnpm changeset                      # pick @wireface/chat, the bump, and a line for its users
pnpm changeset version              # writes packages/chat/CHANGELOG.md and the new version
pnpm --filter @wireface/chat build
pnpm changeset publish

Add a changeset in the same change as the code it describes, whenever the change reaches users of the package. Private packages are left out of versioning ("privatePackages": false in .changeset/config.json).

The server's own version is written in a few places, which change together for a release: the root package.json, apps/server/package.json, apps/server/src/version.ts (shown in /healthz, the admin panel and the OpenAPI spec), the client version in apps/server/src/agent/tools/mcp-pool.ts, the version in the nav and footer of docs/.vitepress/config.ts, and the changelog. The widget has a version of its own, in packages/widget/package.json: see the next section.

Released loaders ​

packages/widget/releases/ keeps the loader of every released widget version, widget@<version>.js, and index.json, which maps each version to its SRI hash (sha384-...). The server serves /widget@x.y.z.js from the current build or, for an older version, from this folder. That is how a page pinned to an old version keeps working after the server is upgraded (see Pinned version and SRI).

These files are committed, and pages load them with an integrity attribute, so a single changed byte breaks every page pinned to that version. Never reformat or edit them. They are excluded from Biome (!packages/widget/releases in biome.json) for that reason; keep your editor's format-on-save off them too.

The widget build (scripts/manifest.mjs) copies the new loader into releases/. When widget@<version>.js already exists with different contents:

  • with CI or RELEASE=1 set, the build fails: bump the version in packages/widget/package.json;
  • in a development build, the file is replaced, with a warning.

So after a local build that changed the loader, git status shows a change to a released file. Don't commit it; restore it:

sh
git restore packages/widget/releases

To release a loader change, bump version in packages/widget/package.json, build with RELEASE=1, and commit the new widget@<version>.js and its line in index.json:

sh
RELEASE=1 pnpm --filter @wireface/widget build

A pinned loader opens whatever chat frame the current server serves, so the loader-to-frame messages (packages/protocol/src/port.ts) must stay compatible with every released loader (see Protocol and engines).

Wireface Core ​

vendor/wireface-core is a copy of Wireface Core, the face engine, so this repo builds without it. Only scripts/sync-core.mjs changes it:

sh
pnpm sync-core          # copy from ../core (or from WIREFACE_CORE_DIR)
pnpm sync-core:check    # exit 1 if vendor/wireface-core differs from ../core

The sync copies src/, assets/skins, config/face.schema.json, LICENSE.md, NOTICE and README.md, and writes the rest: package.json (the @wireface/core workspace package), index.d.ts (from scripts/core-types.d.ts, the types for the parts of core the chatbot uses), core-manifest.json and VERSION.json (the version, a hash of the copied files, core's commit and the time). The server serves the copy at /core/<contentHash>/ with an immutable cache, so a new sync gets a new address.

Never edit vendor/wireface-core by hand: the next sync replaces it, and the files would no longer match their hash. Change Wireface Core in its own repo and sync. To change the types the chatbot sees, edit scripts/core-types.d.ts and sync. pnpm sync-core:check passes, with a message, when ../core isn't there. vendor/ is excluded from Biome, and the copy keeps core's licence (vendor/wireface-core/LICENSE.md).

Docker ​

docker/Dockerfile builds one image with the server, the widget, the admin panel, the docs and the face engine:

  1. build (on node:24-bookworm-slim): pnpm through Corepack, pnpm fetch from the lockfile, then pnpm install --offline --frozen-lockfile, pnpm build and pnpm docs:build, and finally pnpm --filter @wireface/server deploy --prod --legacy for the server with only its production dependencies.
  2. run: the server, the built admin panel and widget (with releases/), vendor/wireface-core and the built docs, copied into the repo's layout. pnpm-workspace.yaml is copied too: the server finds the repo root by it, and the static files from there. It sets NODE_ENV=production, HOST=0.0.0.0, PORT=8800 and DATA_DIR=/data, runs as node, and checks /healthz.

docker/compose.yml runs it as one service, wireface-chat, with the repo root's .env (if there is one) and a wireface-data volume at /data. .dockerignore keeps node_modules, build output, data and .env out of the build context.

sh
docker build -f docker/Dockerfile -t wireface-chat .
docker compose -f docker/compose.yml up -d --build

Two things in the build catch changes that look harmless: --frozen-lockfile fails when pnpm-lock.yaml is out of date, so commit it with any dependency change, and pnpm docs:build runs the reference generator, so notes that drift from the code fail the image (see Testing). Running the image is covered in Docker.

Database migrations ​

The schema is apps/server/src/db/schema.ts; the migrations are in apps/server/drizzle. To change the database:

  1. Edit schema.ts.

  2. Generate the migration (drizzle-kit, configured by apps/server/drizzle.config.ts):

    sh
    pnpm --filter @wireface/server db:generate --name short_description

    It writes drizzle/NNNN_short_description.sql, a snapshot in drizzle/meta, and an entry in drizzle/meta/_journal.json.

  3. Read the SQL it wrote. SQLite's ALTER TABLE can do little, so some changes become "create a new table, copy the rows, drop the old one", which is slow on a big table such as messages.

  4. Commit the SQL file, the snapshot and the journal together.

The server applies new migrations when it starts (openDb() in db/client.ts), and every test's in-memory database runs all of them, so pnpm test checks the whole chain.

Never edit a migration that has shipped. A server records each migration it has applied, and doesn't run one again: an edited migration would leave existing databases different from new ones. Change the schema again and generate a new migration instead.

Some more rules:

  • For SQL that Drizzle can't express, such as the FTS5 tables and triggers in 0001_fts.sql, generate an empty migration and write it by hand: pnpm --filter @wireface/server db:generate --custom --name short_description.
  • A new table whose rows belong to a workspace needs a workspace_id column and a place in WORKSPACE_TABLES in schema.ts: deleting a workspace deletes from each of those tables, and a test (apps/server/test/wireface-sso.test.ts) checks the list is complete.
  • A new column that holds a sealed secret goes in db/rekey.ts too, so that master key rotation re-encrypts it.
  • There are no down migrations. Operators back up before an upgrade and restore to go back (see Upgrades).

The hosted service ​

chat.wireface.dev runs this repo on the wireface.dev web server, next to the website's other services. deploy/ has everything it takes:

filewhat it is
deploy/deploy.ps1Run from Windows: uploads a git archive of a commit and runs remote.sh on the server
deploy/remote.shOn the server, as root: unpacks to /srv/accounts/openface/apps/chat, pnpm install, pnpm build and pnpm docs:build, then installs the unit and the nginx site, restarts the service and checks /healthz
deploy/openface-chat.serviceThe systemd unit: the server on 127.0.0.1:8797 as the openface user, its data in /srv/accounts/openface/data/chat, and the settings for a public service (PUBLIC_URL, TRUST_PROXY, wireface.dev sign-in, the HOSTED_* limits, the demo)
deploy/nginx-chat.wireface.dev.confThe nginx site: TLS with wireface.dev's wildcard certificate, rate limits, and the two WebSockets (/v1/widget/ws, /api/v1/live)
deploy/demo-bot.json, deploy/demo-bot.mjsThe public demo bot that wireface.dev/chat/ embeds: its settings, and a script that makes or updates it and publishes it

To deploy what's committed:

powershell
powershell -NoProfile -File deploy\deploy.ps1            # HEAD
powershell -NoProfile -File deploy\deploy.ps1 -Ref main  # or a branch, tag or commit

It deploys a commit, not your working copy, so commit first. The build runs on the server (Node 22 there), and the old copy stays in place until the new one has built. remote.sh only installs the nginx site when it has changed, and puts the old one back if nginx -t refuses the new one.

Secrets ​

The unit reads /root/.secrets/openface-chat.env (root only, mode 600) before it drops to the openface user:

variablewhat it is
WIREFACE_MASTER_KEYEncrypts the keys stored in the database. Back it up: lose it and every stored key has to be entered again.
SETUP_TOKENFor the first run's owner account.
WIREFACE_ACCOUNTS_SECRETShared with wireface.dev's accounts service (its CHAT_SSO_SECRET), for signing in with a wireface.dev account.
GEMINI_API_KEYThe owner workspace's Gemini key (connected at setup), which the public demo bot runs on.
DEMO_SANDBOX_GEMINI_KEYThe key "Try the demo" sandboxes run on (can be the same key).

Write it from Git Bash without putting secrets on a command line, for example:

sh
printf 'WIREFACE_MASTER_KEY=%s\n' "$(openssl rand -base64 32)" | C:/Windows/System32/OpenSSH/ssh.exe root@ssh.compsmart.cloud 'umask 077; cat >> /root/.secrets/openface-chat.env'

The demo bot ​

After the first deploy, complete the setup (the owner account, with the Gemini key connected from the environment), make an API token with bots:read, bots:write and knowledge:write under Settings, API tokens, then:

sh
WFC_TOKEN=wft_... node deploy/demo-bot.mjs

It makes the bot from deploy/demo-bot.json (or brings it up to date), adds the docs as its knowledge (a crawl of https://chat.wireface.dev/docs/), publishes it and prints its public id: the data-bot in site/chat/index.html. Run it again after changing the file. The bot only works on wireface.dev and chat.wireface.dev (its allowed origins), and has daily caps, so the owner's key can't be run up.

Wireface Chat 0.1.0. These docs are served by your own server.