@vex-chat/spire
v5.0.0
Published
Vex server implementation in NodeJS.
Readme
@vex-chat/spire
Reference server implementation for the Vex protocol.
What's in the box
- REST API (Express 5) for full e2e messaging including auth, registration, users, servers, channels, invites, and file upload.
Install
Spire is part of the vex-protocol/vex-protocol monorepo. Clone the monorepo and install workspace deps:
git clone [email protected]:vex-protocol/vex-protocol
cd protocol
pnpm installSpire's source lives at apps/spire/. Most commands below run from the monorepo root.
Running the server (Docker)
The Dockerfile uses the monorepo as build context so the build can resolve workspace:^ deps for @vex-chat/{types,crypto,libvex}. From apps/spire/, with Docker and Docker Compose installed:
cp .env.example .env
# set SPK, JWT_SECRET, DB_TYPE, … (see Configuration)
docker compose up --buildCompose builds the image (context: monorepo root, dockerfile: apps/spire/Dockerfile), starts Spire with a persistent spire-data volume mounted at /data (SQLite + files/, avatars/, emoji/), and fronts it with nginx on host port 16777. Runtime keys and passkey settings come from apps/spire/.env via Compose env_file; they are intentionally not copied into the Docker image.
Running without Docker
For local development or if you installed from npm, Spire runs with node --experimental-strip-types (no separate compile step):
pnpm --filter @vex-chat/spire start
# or, from apps/spire/: pnpm start
# or: node --experimental-strip-types src/run.tsFrom an npm install, sources live under node_modules/@vex-chat/spire/src/:
node --experimental-strip-types node_modules/@vex-chat/spire/src/run.tsConfiguration
Spire reads configuration from environment variables. Docker Compose: put them in a .env file next to docker-compose.yml (the env_file entry injects them into the container). Bare Node: dotenv loads .env from the process working directory when you run src/run.ts.
Required
| Variable | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SPK | Server Ed25519 private key, hex-encoded. Generate it with pnpm --filter @vex-chat/spire gen-spk; the command also prints a separate JWT_SECRET. |
| JWT_SECRET | Hex or string used as the HMAC secret for JWTs — required and must be separate from SPK. pnpm --filter @vex-chat/spire gen-spk emits a dedicated value; do not reuse SPK here. |
| DB_TYPE | sqlite3 or sqlite3mem. All values use SQLite via better-sqlite3 (file or :memory:). |
Optional
| Variable | Default | Description |
| ------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API_PORT | (see text) | If unset, Spire listens on 16777. Set explicitly to override. |
| NODE_ENV | (unset) | Set to production to disable interactive /docs / /async-docs. If unset or any other value, doc viewers are mounted. helmet() runs in all modes. |
| CORS_ORIGINS | (empty) | Comma-separated allowed Origin values. In production, an empty value rejects cross-origin browser requests; same-origin and native clients are unaffected. Development permits only localhost, loopback, Tauri, and Capacitor origins when unset. |
| SPIRE_TRUST_PROXY_HOPS | 0 | Number of trusted reverse-proxy hops. Set 1 for the bundled single-nginx topology. This must match the real network path so clients cannot spoof X-Forwarded-For to evade IP rate limits. |
| DEV_API_KEY | (empty) | When set, requests that send header x-dev-api-key with the same value (constant-time compare) skip in-process rate limiters. The same gate enables GET /status/process (404 without a valid key): a small JSON snapshot of the Spire Node process (PID, uptime, memoryUsage, cumulative resourceUsage, WebSocket client count). Dev / load-testing only — never set in production. |
| CANARY | (unset) | |
Passkeys / WebAuthn
The /auth/passkey/*, /user/:id/passkeys/*, and /passkey/* routes are gated on SPIRE_PASSKEY_RP_ID and SPIRE_PASSKEY_ORIGINS; without them those endpoints return 500 Passkeys are not configured. Spire also serves the CLI WebAuthn bridge at /cli/passkey, which should be loaded from an origin listed in SPIRE_PASSKEY_ORIGINS so browser passkey ceremonies verify against the same RP config as native clients. See .env.example for the full set, including the optional SPIRE_PASSKEY_IOS_APP_IDS, SPIRE_PASSKEY_ANDROID_PACKAGE, and SPIRE_PASSKEY_ANDROID_FINGERPRINTS triple that makes spire serve the WebAuthn well-known association files (/.well-known/apple-app-site-association, /.well-known/assetlinks.json) directly. Those endpoints 404 when their env vars aren't set, so a non-passkey deployment is indistinguishable from one that hasn't enrolled an app.
The same SPIRE_PASSKEY_ANDROID_FINGERPRINTS value is reused by getRpConfig() to derive android:apk-key-hash:<base64url> entries, which it merges into the WebAuthn expectedOrigin allowlist. Native Android Credential Manager sets clientDataJSON.origin to that string instead of the RP host, so without those entries simplewebauthn rejects every native-Android assertion at the origin check (the mobile UI surfaces this as a generic "RP failed" error). Operators only have to publish the cert fingerprints; the base64url math is handled server-side.
For Docker Compose, place the passkey variables in apps/spire/.env beside SPK and JWT_SECRET; the compose file passes that env file into the spire container. Production should use the production RP host, Android package, and EAS signing-certificate fingerprint. Development should use its own host/package/fingerprint so the dev APK is not authorized by the production association file.
Sample .env
# Run `pnpm --filter @vex-chat/spire gen-spk` and paste the two lines it prints (SPK + JWT_SECRET).
SPK=a1b2c3...
JWT_SECRET=d4e5f6...
DB_TYPE=sqlite
# CANARY=true
# API_PORT= # unset = 16777 unless you override
NODE_ENV=productionDevelopment
From the monorepo root (or use pnpm <script> from apps/spire/):
pnpm install # install workspace deps
pnpm --filter @vex-chat/spire build # tsc (sanity check — runtime uses --experimental-strip-types)
pnpm --filter @vex-chat/spire lint # eslint strictTypeChecked
pnpm --filter @vex-chat/spire lint:fix # eslint --fix
pnpm --filter @vex-chat/spire test # vitest runWorkspace-wide commands from root: pnpm test, pnpm lint, pnpm format:check, pnpm -r build.
See the root AGENTS.md and this package's AGENTS.md for the release flow (changesets → publish via OIDC) and the rules for writing changesets.
Outside contributors should follow the root CONTRIBUTING.md (including the CLA).
License
Open source default: AGPL-3.0 (full text; see package.json for SPDX). Commercial licenses from Vex Heavy Industries LLC: LICENSE-COMMERCIAL, LICENSING.md, vex.wtf.
