Skip to content

Demo Site VPS Deploy Manual ​

Complete manual for deploying the online demo site of Familiars (demo.familiars.cn) on a CPU-only VPS. The demo site runs the full Electron-rendered SPA's VITE_DEMO_MODE web build + a CPU-only bridge (no local models loaded), with nginx same-origin /api reverse proxy to the bridge (proxy_buffering off to preserve SSE chat streams).

The demo is functionally complete — file generation, CreativeWorkshop, scheduling, todos, memory RAG, browser automation all work for real in the isolated demo workspace. Only OS-control tools (shell execution, input control, phone, social) and GPU-intensive features are blocked.


Architecture ​

Browser → Cloudflare → Caddy/nginx :443 (TLS)
 └── / → Static SPA (electron/dist, built with `VITE_DEMO_MODE=1`)
 └── /api/* → Reverse proxy to 127.0.0.1:18711 (bridge, FAMILIARS_HTTP_INSECURE=1)
 proxy_buffering off (SSE chat/status streams)
  • The demo bridge is an independent process, with dedicated WORKSPACE_ROUTE + FAMILIARS_DB_PATH (completely isolated from production / your local machine).
  • FAMILIARS_APPROVAL_DISABLED=1 is required for the demo — the block-list in src/bridge/routes/demo.py handles security by preventing dangerous tools (shell execution, OS control, phone, social) while allowing everything else to run for real.
  • Never expose POST /api/demo/token to non-demo environments — that endpoint returns the current workspace's bridge token.

Prerequisites ​

  • A VPS (2C4G is sufficient; no GPU needed), Ubuntu 22.04/24.04.
  • Domain: demo.familiars.cn (add an A record pointing to VPS IP; if using Cloudflare then orange cloud + Caddy).
  • Local repo has electron/dist (built with VITE_DEMO_MODE=1) and src/.

Step 1: Build Demo SPA ​

On your local machine (repo root):

bash
cd electron
VITE_DEMO_MODE=1 npm run build # Windows: set VITE_DEMO_MODE=1 && npm run build

Output: electron/dist/ (pure static Vue SPA, base: './', auto-uses window.location.origin when no electronAPI, calls POST /api/demo/token at startup to get the public demo token).

Step 2: Prepare Server ​

bash
# System dependencies (sqlite is built into Python, mainly need python3.11 + venv)
apt update && apt install -y python3.11 python3.11-venv nginx

Create directories and copy files:

bash
mkdir -p /opt/familiars-demo/{app,spa,data}
# Copy src/ and requirements.txt from local machine
scp -r src/ root@<VPS>:/opt/familiars-demo/app/
scp requirements.txt root@<VPS>:/opt/familiars-demo/
scp -r electron/dist root@<VPS>:/opt/familiars-demo/spa/

Step 3: Install Dependencies + Write Environment ​

bash
cd /opt/familiars-demo
python3.11 -m venv .venv
.venv/bin/pip install -r requirements.txt

.env (demo-specific; configure LLM endpoint as you prefer):

bash
# LLM (also needed for demo — agent dialogue/generation goes through this OpenAI-compatible endpoint)
BASE_URL=https://your-llm-gateway/v1
API_KEY=sk-xxxx
DEFAULT_MODEL=your-model-name
MANAGER_MODEL=your-model-name

# Demo isolation (critical)
FAMILIARS_DB_PATH=/opt/familiars-demo/data/demo.db
WORKSPACE_ROUTE=/opt/familiars-demo/data/workspace

# Bridge falls back to plain HTTP (nginx on same machine does /api reverse proxy, no need for HTTPS)
FAMILIARS_HTTP_INSECURE=1

# Per-tenant: official bridge separates WORKSPACE_ROUTE/FAMILIARS_DB_PATH by user $HOME
# (see "Per-tenant Isolation" at the end of this manual)

WORKSPACE_ROUTE must be compatible with src.base.config's default workspace layout — it will generate agents/, familiars.db (if FAMILIARS_DB_PATH not specified), etc. under this directory. Demo separates users by running separate processes.

Step 4: Start Bridge (CPU-only) ​

bash
cd /opt/familiars-demo
WORKSPACE_ROUTE=/opt/familiars-demo/data/workspace \
FAMILIARS_DB_PATH=/opt/familiars-demo/data/demo.db \
FAMILIARS_HTTP_INSECURE=1 \
FAMILIARS_BROKER_BIND=0.0.0.0 \
FAMILIARS_APPROVAL_DISABLED=1 \
.venv/bin/python -m src.bridge.run

FAMILIARS_BROKER_BIND=0.0.0.0 must be set. Bridge defaults to listening on 127.0.0.1 only (src/bridge/run.py:_bind_host), but nginx/Caddy is another container and can only reach back via the host bridge (172.18.0.1 / host.docker.internal) — if bridge binds to loopback, the reverse proxy will definitely 502. After binding 0.0.0.0, make absolutely sure your firewall / security group does not expose 18711 to the public internet (POST /api/demo/token returns that workspace's bridge token, publicly reachable means complete takeover of demo data permissions).

FAMILIARS_APPROVAL_DISABLED=1: the demo security model is block-list + approval disabled. The block-list (_DEMO_BLOCKED_TOOLS in src/bridge/routes/demo.py) prevents shell execution, OS input control, phone/social sending, SSH/SCP, and GPU-intensive generation. Everything else — file generation (Word/PPT/Excel), write/edit, CreativeWorkshop, scheduling, browser automation, HTTP requests — runs for real inside the isolated demo workspace.

System service (systemd, /etc/systemd/system/familiars-demo.service):

ini
[Unit]
Description=Familiars Demo Bridge
After=network.target

[Service]
User=root
WorkingDirectory=/opt/familiars-demo
EnvironmentFile=/opt/familiars-demo/.env
Environment=FAMILIARS_BROKER_BIND=0.0.0.0
Environment=FAMILIARS_APPROVAL_DISABLED=1
ExecStart=/opt/familiars-demo/.venv/bin/python -m src.bridge.run
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
bash
systemctl daemon-reload && systemctl enable --now familiars-demo
curl http://127.0.0.1:18711/api/demo/status # Expected: {"ok":true,...}
curl http://172.18.0.1:18711/api/demo/status # Container-side verification (same path as Caddy reverse proxy)

Step 5: nginx Same-origin Reverse Proxy + Static Site ​

/etc/nginx/sites-available/familiars-demo:

nginx
server {
 listen 80;
 server_name demo.familiars.cn;
 root /opt/familiars-demo/spa;

 # —— SPA ——
 location / {
 try_files $uri $uri/ /index.html;
 }

 # —— /api reverse proxy to bridge (SSE requires buffering off) ——
 location /api/ {
 proxy_pass http://127.0.0.1:18711;
 proxy_http_version 1.1;
 proxy_buffering off;
 proxy_cache off;
 proxy_set_header Host $host;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_read_timeout 300s;
 proxy_send_timeout 300s;
 }

 client_max_body_size 50m;
}
bash
ln -s /etc/nginx/sites-available/familiars-demo /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

If using Cloudflare and want HTTPS, just add certs to listen 80; or add a Caddy layer. The core remains: same-origin /api reverse proxy + proxy_buffering off.

Verification (Expected Live State) ​

Opening https://demo.familiars.cn/ in a browser should show the SPA:

  1. Top banner says "共享云端演示实例" and explains that chat / memory / schedule / file generation / CreativeWorkshop work for real; DEMO模式不支持本地操作 — file read/write, code execution, browser, phone, social all return the fixed rejection message;
  2. First screen POST /api/demo/token → gets demo_session_* token → X-Bridge-Token set up;

Demo token only allows /api/demo/* and /api/chat/*, all other /api/* return 401 — the public token cannot access any other data in the workspace (will not be used as the bridge's global workspace token).

  1. Send a message to agent 0 → SSE /api/chat/stream replies character by character (observe nginx logs for no buffering stalls);
  2. Ask the agent to generate a Word/PPT/Excel file → the generate_* tool runs for real inside the demo workspace, and the generated file is available for download;
  3. Open CreativeWorkshop → recipe list, editor, compile/dryrun all functional;
  4. Ask the agent to read a file or run a shell command → gets the fixed "DEMO模式不支持本地操作" message;
  5. Browser tools, phone tools, SSH/SCP — all return the same fixed rejection.

Session Isolation ("One Shared Workspace + Independent Tokens") ​

The demo uses shared-workspace + per-session token isolation, not one-process-per-user:

  • All visitors connect to the same demo bridge process, same isolated workspace (WORKSPACE_ROUTE).
  • Each person calls /token to get an independent demo_session_* token (6-hour expiry); this token only allows /api/demo/* (status/feedback) and /api/chat/* (chat SSE).
  • Chat, todos, memory RAG, file generation, CreativeWorkshop, scheduling all run on the real backend in the shared workspace — so read-only data (like agent lists, memory index) is the same for all visitors; while blocked tools (shell execution, OS control, phone, social) return a synthetic rejection via the demo gate's block-list.

Trade-off: Solo-agent chat with DemoWorklogScope is purely sample data; if in the future you want "each user truly has their own memory space", you'd need to go back to per-tenant processes (independent WORKSPACE_ROUTE + FAMILIARS_DB_PATH), wrapping Step 4 in run-user.sh separated by user — see next section.

Per-tenant Isolation (Multi-user Version) ​

The current demo already uses shared-workspace + demo token (see previous section) — only switch to the per-process model below when you want "each user has their own independent memory/task space".

To give each demo user an independent database (to avoid seeing each other's chats/tasks), wrap the Step 4 startup command in a /opt/familiars-demo/run-user.sh, separated by $1 (username):

bash
#!/usr/bin/env bash
USER="$1"
DB="/opt/familiars-demo/data/users/${USER}/demo.db"
WS="/opt/familiars-demo/data/users/${USER}/workspace"
mkdir -p "$(dirname "$DB")" "$WS"
WORKSPACE_ROUTE="$WS" FAMILIARS_DB_PATH="$DB" FAMILIARS_HTTP_INSECURE=1 FAMILIARS_BROKER_BIND=0.0.0.0 \
 /opt/familiars-demo/.venv/bin/python -m src.bridge.run

Then start a systemd instance for each user or wrap with another process manager (supervisord program group, @ service template both work). Each process gets an independent port, nginx uses different location/subpath (or subdomain) to route.

Demo Data Bootstrap (demo_bootstrap) ​

The guild rooms actually needed by the dialogue need to be pre-seeded in demo data. After first startup, run:

bash
WORKSPACE_ROUTE=/opt/familiars-demo/data/workspace FAMILIARS_HTTP_INSECURE=1 \
 FAMILIARS_DB_PATH=/opt/familiars-demo/data/demo.db \
 .venv/bin/python -c "
from src.bridge.deps import get_broker_hub
from src.broker.workshop import repos as wr
conn = get_broker_hub().conn
# Example: create one room each for feedback/demo (fixed room_id, 'FAMILIAR' room as public feedback receiver)
wr.create_room(conn, broker_id='<broker_id>', name='Familiar', room_id='FAMILIAR-DEMO',
 created_by_cluster_id='demo')
print('rooms ok')
"

Two fixed rooms (Familiar=public feedback/demo; feedback-official=only visible to official bot owner) should be pre-seeded in deploy docs with bot wallet ≥20 VTP (auto_reward_feedback rewards 20 per task, insufficient balance marks reward_pending, does not block publishing).

Operations Quick Reference ​

OperationCommand
Restart bridgesystemctl restart familiars-demo
View logsjournalctl -u familiars-demo -f
Update SPALocal VITE_DEMO_MODE=1 npm run build → scp -r electron/dist/* to /opt/familiars-demo/spa/
Update bridge codescp -r src/ to /opt/familiars-demo/app/ → restart service
BackupSnapshot /opt/familiars-demo/data/

Familiars · 多 Agent AI 桌面应用