Appearance
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 publishAdd 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
CIorRELEASE=1set, the build fails: bump the version inpackages/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/releasesTo 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 buildA 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 ../coreThe 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:
- build (on
node:24-bookworm-slim): pnpm through Corepack,pnpm fetchfrom the lockfile, thenpnpm install --offline --frozen-lockfile,pnpm buildandpnpm docs:build, and finallypnpm --filter @wireface/server deploy --prod --legacyfor the server with only its production dependencies. - run: the server, the built admin panel and widget (with
releases/),vendor/wireface-coreand the built docs, copied into the repo's layout.pnpm-workspace.yamlis copied too: the server finds the repo root by it, and the static files from there. It setsNODE_ENV=production,HOST=0.0.0.0,PORT=8800andDATA_DIR=/data, runs asnode, 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 --buildTwo 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:
Edit
schema.ts.Generate the migration (drizzle-kit, configured by
apps/server/drizzle.config.ts):shpnpm --filter @wireface/server db:generate --name short_descriptionIt writes
drizzle/NNNN_short_description.sql, a snapshot indrizzle/meta, and an entry indrizzle/meta/_journal.json.Read the SQL it wrote. SQLite's
ALTER TABLEcan 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 asmessages.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_idcolumn and a place inWORKSPACE_TABLESinschema.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.tstoo, 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:
| file | what it is |
|---|---|
deploy/deploy.ps1 | Run from Windows: uploads a git archive of a commit and runs remote.sh on the server |
deploy/remote.sh | On 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.service | The 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.conf | The 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.mjs | The 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 commitIt 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:
| variable | what it is |
|---|---|
WIREFACE_MASTER_KEY | Encrypts the keys stored in the database. Back it up: lose it and every stored key has to be entered again. |
SETUP_TOKEN | For the first run's owner account. |
WIREFACE_ACCOUNTS_SECRET | Shared with wireface.dev's accounts service (its CHAT_SSO_SECRET), for signing in with a wireface.dev account. |
GEMINI_API_KEY | The owner workspace's Gemini key (connected at setup), which the public demo bot runs on. |
DEMO_SANDBOX_GEMINI_KEY | The 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.mjsIt 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.