openvibers/openvibe.bot Commits
Add production deploy files for Bot (systemd unit, nginx site, README)
Changes
@@@ -1,3 +1,14 @@# OpenVibe.BotThe open control panel for robots (openvibe.bot). Design: OpenVibe.Contracts docs/adr/ADR-043-bot-devices-and-control.md; plan track T15.++++## Deploy++++Production runs the Node process under systemd as `openvibe-bot.service` (unit reference: `deploy/systemd/openvibe-bot.service`), with `WorkingDirectory=/opt/openvibe.bot`, `ExecStart` running `server/index.js`, and secrets/config read from the environment file `/etc/openvibe/bot.env`. The app listens on port **4630** behind nginx (`deploy/nginx/openvibe.bot.conf`), which terminates TLS and upgrades the `/device` and `/control` WebSockets to `127.0.0.1:4630`. Readiness is `http://127.0.0.1:4630/api/ready` (liveness: `/api/health`); both are loopback-only.++++To run the schema migrations before the service serves, start it once with the direct owner connection in `/etc/openvibe/bot.env` (`DATABASE_DIRECT_URL`); the boot applies `migrations/` and then serves on the pooled `DATABASE_URL`.++++## Deploy files++++- `deploy/systemd/openvibe-bot.service` — systemd unit (`EnvironmentFile=/etc/openvibe/bot.env`, `PORT=4630`, hardening and `Restart=always`).++- `deploy/nginx/openvibe.bot.conf` — nginx reference site for openvibe.bot: TLS, www→apex, rate limits, and WebSocket upgrade headers for the device (`/device`) and operator (`/control`) sockets, passing the `Authorization` header through.nnew file mode 100644@@@ -0,0 +1,129 @@++## ═══════════════════════════════════════════════════════════════++## openvibe.bot — Nginx configuration (REFERENCE)++##++## Deployed to /etc/nginx/sites-available/openvibe.bot.conf — keep them in sync and reload nginx++## after changing it. The Node app (port 4630, systemd unit openvibe-bot) serves the REST API and++## terminates the device (`/device`) and operator (`/control`) WebSockets; nginx terminates TLS,++## redirects www → apex, upgrades the two WebSocket routes, rate-limits sign-in and the API, and++## compresses text.++##++## No proxy_cache: the app decides cacheability per response (no-store on the API), and device and++## operator sockets must never be buffered or cached. Never log request bodies or the Authorization++## header here: the device socket carries its credential in the upgrade's Authorization header.++## ═══════════════════════════════════════════════════════════════++++# WebSocket upgrade: `Connection` is hop-by-hop, so it is derived from the client's request per++# connection. This map must live in the http context (alongside the limit zones), not in a server.++map $http_upgrade $connection_upgrade {++ default upgrade;++ '' close;++}++++limit_req_zone $binary_remote_addr zone=ovbot_auth:10m rate=10r/m;++limit_req_zone $binary_remote_addr zone=ovbot_api:10m rate=10r/s;++++server {++ listen 80;++ listen [::]:80;++ server_name openvibe.bot www.openvibe.bot;++++ location /.well-known/acme-challenge/ { root /var/www/certbot; }++ location / { return 301 https://openvibe.bot$request_uri; }++}++++server {++ listen 443 ssl;++ listen [::]:443 ssl;++ http2 on;++ server_name www.openvibe.bot;++++ ssl_certificate /etc/letsencrypt/live/openvibe.bot/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.bot/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++++ return 301 https://openvibe.bot$request_uri;++}++++server {++ listen 443 ssl;++ listen [::]:443 ssl;++ http2 on;++ server_name openvibe.bot;++++ ssl_certificate /etc/letsencrypt/live/openvibe.bot/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.bot/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++++ access_log /var/log/nginx/openvibe.bot.access.log;++ error_log /var/log/nginx/openvibe.bot.error.log;++++ # The API accepts at most 64 kB of JSON (server/app.js); 1 MB leaves room for headers.++ client_max_body_size 1m;++++ add_header X-Content-Type-Options nosniff always;++ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;++++ gzip on;++ gzip_proxied any;++ gzip_vary on;++ gzip_comp_level 5;++ gzip_min_length 1024;++ gzip_types text/css application/javascript text/javascript application/json application/problem+json application/schema+json image/svg+xml text/plain application/xml;++++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $remote_addr;++ proxy_set_header CF-Connecting-IP $remote_addr;++ proxy_set_header X-Forwarded-Proto $scheme;++ # The device socket authenticates with the credential in this header; forward it verbatim.++ proxy_set_header Authorization $http_authorization;++++ # ── Device WebSocket: one outbound connection per device ──++ # Authenticated by `Authorization: Bearer <credential>` on the upgrade (never a query string).++ # Sockets are long-lived and bidirectional, so buffering is off and reads never time out.++ location = /device {++ proxy_pass http://127.0.0.1:4630;++ proxy_set_header Upgrade $http_upgrade;++ proxy_set_header Connection $connection_upgrade;++ proxy_buffering off;++ proxy_read_timeout 3600s;++ proxy_send_timeout 3600s;++ }++++ # ── Operator/control WebSocket: private, invite and queue access ──++ location = /control {++ proxy_pass http://127.0.0.1:4630;++ proxy_set_header Upgrade $http_upgrade;++ proxy_set_header Connection $connection_upgrade;++ proxy_buffering off;++ proxy_read_timeout 3600s;++ proxy_send_timeout 3600s;++ }++++ # ── Network SSO (sign-in, callback, sign-out) ───────────++ location /auth/ {++ limit_req zone=ovbot_auth burst=10 nodelay;++ limit_req_status 429;++ proxy_pass http://127.0.0.1:4630;++ }++++ # ── REST API ────────────────────────────────────────────++ location /api/ {++ limit_req zone=ovbot_api burst=20 nodelay;++ limit_req_status 429;++ proxy_pass http://127.0.0.1:4630;++ }++++ # ── Prometheus metrics: host-local scrapers only (http://127.0.0.1:4630/metrics) ──++ location = /metrics { return 404; }++++ # ── Everything else (the panel, release.json, static assets) ──++ location / {++ proxy_pass http://127.0.0.1:4630;++ }++++ location ~ /\.(?!well-known) {++ deny all;++ }++}nnew file mode 100644@@@ -0,0 +1,31 @@++[Unit]++Description=OpenVibe.Bot — the open control panel for robots (robots, devices, pairing, the operator and device WebSockets)++# Network (sign-in, JWKS, OAuth) should be up first, and Bot's own PostgreSQL (pooled runtime role plus the++# direct owner role for migrations) and Valkey (state shared between processes) are required to serve; the++# events relay waits in the outbox. Bot degrades honestly without Events: routes that need it answer 503++# (see /api/ready).++After=network.target postgresql.service pgbouncer.service valkey-server.service++Wants=openvibe-network.service postgresql.service pgbouncer.service valkey-server.service++++[Service]++Type=simple++User=ubuntu++Group=ubuntu++WorkingDirectory=/opt/openvibe.bot++EnvironmentFile=/etc/openvibe/bot.env++Environment=NODE_ENV=production++Environment=PORT=4630++Environment=PATH=/usr/local/bin:/usr/bin:/bin++ExecStart=/usr/bin/env node /opt/openvibe.bot/server/index.js++Restart=always++RestartSec=5++KillSignal=SIGTERM++TimeoutStopSec=10++NoNewPrivileges=true++PrivateTmp=true++ProtectSystem=strict++ProtectHome=true++# Bot's own database is PostgreSQL on the host's data role; the code tree stays read-only.++++[Install]++WantedBy=multi-user.target
@@ -1,3 +1,14 @@
@@ -1,3 +1,14 @@
# OpenVibe.Bot
# OpenVibe.Bot
The open control panel for robots (openvibe.bot). Design: OpenVibe.Contracts docs/adr/ADR-043-bot-devices-and-control.md; plan track T15.
The open control panel for robots (openvibe.bot). Design: OpenVibe.Contracts docs/adr/ADR-043-bot-devices-and-control.md; plan track T15.
+
+## Deploy
+
+Production runs the Node process under systemd as `openvibe-bot.service` (unit reference: `deploy/systemd/openvibe-bot.service`), with `WorkingDirectory=/opt/openvibe.bot`, `ExecStart` running `server/index.js`, and secrets/config read from the environment file `/etc/openvibe/bot.env`. The app listens on port **4630** behind nginx (`deploy/nginx/openvibe.bot.conf`), which terminates TLS and upgrades the `/device` and `/control` WebSockets to `127.0.0.1:4630`. Readiness is `http://127.0.0.1:4630/api/ready` (liveness: `/api/health`); both are loopback-only.
+
+To run the schema migrations before the service serves, start it once with the direct owner connection in `/etc/openvibe/bot.env` (`DATABASE_DIRECT_URL`); the boot applies `migrations/` and then serves on the pooled `DATABASE_URL`.
+
+## Deploy files
+
+- `deploy/systemd/openvibe-bot.service` — systemd unit (`EnvironmentFile=/etc/openvibe/bot.env`, `PORT=4630`, hardening and `Restart=always`).
+- `deploy/nginx/openvibe.bot.conf` — nginx reference site for openvibe.bot: TLS, www→apex, rate limits, and WebSocket upgrade headers for the device (`/device`) and operator (`/control`) sockets, passing the `Authorization` header through.
new file mode 100644
new file mode 100644
@@ -0,0 +1,129 @@
@@ -0,0 +1,129 @@
+## ═══════════════════════════════════════════════════════════════
+## openvibe.bot — Nginx configuration (REFERENCE)
+##
+## Deployed to /etc/nginx/sites-available/openvibe.bot.conf — keep them in sync and reload nginx
+## after changing it. The Node app (port 4630, systemd unit openvibe-bot) serves the REST API and
+## terminates the device (`/device`) and operator (`/control`) WebSockets; nginx terminates TLS,
+## redirects www → apex, upgrades the two WebSocket routes, rate-limits sign-in and the API, and
+## compresses text.
+##
+## No proxy_cache: the app decides cacheability per response (no-store on the API), and device and
+## operator sockets must never be buffered or cached. Never log request bodies or the Authorization
+## header here: the device socket carries its credential in the upgrade's Authorization header.
+## ═══════════════════════════════════════════════════════════════
+
+# WebSocket upgrade: `Connection` is hop-by-hop, so it is derived from the client's request per
+# connection. This map must live in the http context (alongside the limit zones), not in a server.
+map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+}
+
+limit_req_zone $binary_remote_addr zone=ovbot_auth:10m rate=10r/m;
+limit_req_zone $binary_remote_addr zone=ovbot_api:10m rate=10r/s;
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name openvibe.bot www.openvibe.bot;
+
+ location /.well-known/acme-challenge/ { root /var/www/certbot; }
+ location / { return 301 https://openvibe.bot$request_uri; }
+}
+
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name www.openvibe.bot;
+
+ ssl_certificate /etc/letsencrypt/live/openvibe.bot/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.bot/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ return 301 https://openvibe.bot$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name openvibe.bot;
+
+ ssl_certificate /etc/letsencrypt/live/openvibe.bot/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.bot/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ access_log /var/log/nginx/openvibe.bot.access.log;
+ error_log /var/log/nginx/openvibe.bot.error.log;
+
+ # The API accepts at most 64 kB of JSON (server/app.js); 1 MB leaves room for headers.
+ client_max_body_size 1m;
+
+ add_header X-Content-Type-Options nosniff always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+ gzip on;
+ gzip_proxied any;
+ gzip_vary on;
+ gzip_comp_level 5;
+ gzip_min_length 1024;
+ gzip_types text/css application/javascript text/javascript application/json application/problem+json application/schema+json image/svg+xml text/plain application/xml;
+
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header CF-Connecting-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The device socket authenticates with the credential in this header; forward it verbatim.
+ proxy_set_header Authorization $http_authorization;
+
+ # ── Device WebSocket: one outbound connection per device ──
+ # Authenticated by `Authorization: Bearer <credential>` on the upgrade (never a query string).
+ # Sockets are long-lived and bidirectional, so buffering is off and reads never time out.
+ location = /device {
+ proxy_pass http://127.0.0.1:4630;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_buffering off;
+ proxy_read_timeout 3600s;
+ proxy_send_timeout 3600s;
+ }
+
+ # ── Operator/control WebSocket: private, invite and queue access ──
+ location = /control {
+ proxy_pass http://127.0.0.1:4630;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_buffering off;
+ proxy_read_timeout 3600s;
+ proxy_send_timeout 3600s;
+ }
+
+ # ── Network SSO (sign-in, callback, sign-out) ───────────
+ location /auth/ {
+ limit_req zone=ovbot_auth burst=10 nodelay;
+ limit_req_status 429;
+ proxy_pass http://127.0.0.1:4630;
+ }
+
+ # ── REST API ────────────────────────────────────────────
+ location /api/ {
+ limit_req zone=ovbot_api burst=20 nodelay;
+ limit_req_status 429;
+ proxy_pass http://127.0.0.1:4630;
+ }
+
+ # ── Prometheus metrics: host-local scrapers only (http://127.0.0.1:4630/metrics) ──
+ location = /metrics { return 404; }
+
+ # ── Everything else (the panel, release.json, static assets) ──
+ location / {
+ proxy_pass http://127.0.0.1:4630;
+ }
+
+ location ~ /\.(?!well-known) {
+ deny all;
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+[Unit]
+Description=OpenVibe.Bot — the open control panel for robots (robots, devices, pairing, the operator and device WebSockets)
+# Network (sign-in, JWKS, OAuth) should be up first, and Bot's own PostgreSQL (pooled runtime role plus the
+# direct owner role for migrations) and Valkey (state shared between processes) are required to serve; the
+# events relay waits in the outbox. Bot degrades honestly without Events: routes that need it answer 503
+# (see /api/ready).
+After=network.target postgresql.service pgbouncer.service valkey-server.service
+Wants=openvibe-network.service postgresql.service pgbouncer.service valkey-server.service
+
+[Service]
+Type=simple
+User=ubuntu
+Group=ubuntu
+WorkingDirectory=/opt/openvibe.bot
+EnvironmentFile=/etc/openvibe/bot.env
+Environment=NODE_ENV=production
+Environment=PORT=4630
+Environment=PATH=/usr/local/bin:/usr/bin:/bin
+ExecStart=/usr/bin/env node /opt/openvibe.bot/server/index.js
+Restart=always
+RestartSec=5
+KillSignal=SIGTERM
+TimeoutStopSec=10
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=strict
+ProtectHome=true
+# Bot's own database is PostgreSQL on the host's data role; the code tree stays read-only.
+
+[Install]
+WantedBy=multi-user.target