Screen-share (send): startScreenShare requests the MediaProjection grant via
startActivityForResult, then CallMedia.setScreenShareEnabled(true,
ScreenCaptureParams(data)) publishes it over the native LiveKit room.
stopScreenShare unpublishes. Both emit screenShareState so the web button
reflects reality (incl. a declined grant). LiveKit's SDK manifest already
provides the mediaProjection foreground service + FOREGROUND_SERVICE_MEDIA_
PROJECTION (merged into the app), so no manifest change is needed.
Received shared screen: implemented setTileZoom (was a no-op) — the web
captures pinch/pan under hole-punch and forwards {scale,tx,ty}; CallVideo
applies it as a renderer transform (scale about centre + translate ×density),
scale<=1 clears it. Mirrors the iOS applyZoom contract. Screen tiles already
render aspect-fit.
APK-only: the web bridge already calls these methods and received-screen
rendering is gated on ANDROID_NATIVE_VIDEO (already on) — no server/web change.
All LiveKit 2.29.2 APIs (setScreenShareEnabled, ScreenCaptureParams) verified
against source. Clarity-when-zoomed (adaptive-stream) is a follow-up if needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
228 lines
15 KiB
Markdown
228 lines
15 KiB
Markdown
# Tech Stack — BizGaze Connect
|
|
|
|
> Living document. **Last updated: 2026-11-03** (rev 6). Update this file whenever a dependency, platform, service, or
|
|
> tool is added/removed/version-bumped. See [Maintaining this file](#maintaining-this-file) and the
|
|
> [Changelog](#changelog) at the bottom.
|
|
|
|
## What this project is
|
|
|
|
BizGaze Connect is a self-hosted team **chat + meetings + calling** product (evolved from a remote
|
|
screen-sharing/"remote-access" tool, whose agent console still lives in the repo). One backend serves a web app
|
|
and is wrapped by native mobile (Capacitor) and desktop (Electron) shells. The UI loads live from the server, so
|
|
web changes deploy **without** rebuilding the native apps.
|
|
|
|
**Architecture style:** thin, dependency-light Node backend (custom modules, no web framework) + a single-page
|
|
vanilla-JS web UI + native shells that wrap the web and add native capabilities (push, calling, screen capture,
|
|
file access). Real-time via WebSockets; meeting media via WebRTC (P2P mesh, or LiveKit SFU when enabled).
|
|
|
|
---
|
|
|
|
## Backend (server)
|
|
|
|
- **Runtime:** Node.js — `engines` requires **>= 22.5**; the Docker image is **`node:24-alpine`**.
|
|
- **HTTP/HTTPS:** Node built-in `http`/`https` modules. **No Express/Koa/Fastify** — a small custom router
|
|
(`routes.js`) maps `"METHOD /path"` to handlers; static files + authenticated downloads via `static.js`.
|
|
- **WebSockets:** [`ws`](https://github.com/websockets/ws) `^8.18` (`WebSocketServer` on `/ws`) — chat, presence,
|
|
meeting signaling (SDP/ICE relay + consent).
|
|
- **Structure (server/):** `server.js` (wiring), `routes.js` (HTTP JSON API), `repos.js` (all SQL / data access),
|
|
`signaling.js` (WS), `calls.js`, `chat.js`, `presence.js`, `session.js`/`auth.js`, `push.js`, `livekit.js`,
|
|
`media.js`, `mailer.js`, `webhooks.js`, `bizgaze.js` (BizGaze identity provider), `directory.js`, `config.js`.
|
|
- **Media tooling:** **ffmpeg** (in the image) for server-side video poster thumbnails.
|
|
|
|
## Data layer
|
|
|
|
- **Database:** **PostgreSQL 16** (`pg` `^8.13`). This is the only backend — **SQLite was retired 2026-08-12**;
|
|
one schema source of truth at `server/db/schema.pg.sql`. Selected via `DB_BACKEND=pg` / `DATABASE_URL`.
|
|
- **DB access:** hand-written SQL through an async adapter (`server/dbx.js`, `server/db/`), centralized in
|
|
`repos.js`. No ORM.
|
|
- **Cache / pub-sub:** **Redis 7** (`redis` `^4.7`) for cross-instance real-time fan-out (chat/presence) —
|
|
**optional**, only when `PUBSUB_BACKEND=redis`; default is in-memory single-instance (`pubsub.js`).
|
|
|
|
## Identity & auth
|
|
|
|
No external auth library — Node's built-in **`crypto`** only (`server/auth.js`): scrypt password hashing (64-byte
|
|
hash, per-user salt, timing-safe compare), opaque random session tokens, SHA-256-hashed token storage, and
|
|
**TOTP MFA** (RFC 6238, SHA-1, 30s, 6 digits) with `otpauth://` enrollment URLs.
|
|
|
|
- **Sessions** (`sessions_auth` table, resolved in `server/session.js`): one opaque bearer token accepted over
|
|
three transports — a `sid` **HttpOnly cookie** (web), `Authorization: Bearer <token>` (native desktop/mobile
|
|
+ WS upgrade), or a `?access_token=`/`?token=` query param (browser WS fallback). **90-day TTL, sliding** —
|
|
any `/api/me` hit re-stamps the cookie and pushes the expiry out, so active users stay signed in indefinitely
|
|
and only lapse after 90 days of no use (or logout).
|
|
- **Native refresh tokens** (`refresh_tokens`): 90-day, one-time-use **with rotation**, stored only as a hash;
|
|
`POST /api/auth/refresh` swaps a refresh token for a fresh access token so mobile/desktop never re-prompt for
|
|
a password.
|
|
- **MFA:** optional TOTP second factor. A `requireMfa` gate in `currentUser` holds a session back until
|
|
`mfa_passed`; flow is `/api/mfa/setup` (secret/QR) → `/api/mfa/enable` (confirm) → `/api/login/mfa` (step-2
|
|
code).
|
|
- **Identity providers:**
|
|
- **Local password** accounts — dev/test and self-contained runs.
|
|
- **BizGaze as IdP** (`server/bizgaze.js`, active when `BIZGAZE_LOGIN_URL` is set): credentials are verified
|
|
against BizGaze's `ValidateAndLogin` and BizGaze becomes the **sole authority** (local passwords rejected
|
|
unless `ALLOW_LOCAL_LOGIN=1`). A local user row is provisioned/synced (name, avatar, role, tenant) on every
|
|
login, keyed on the **BizGaze person-id** (not the typed email/phone) so signing in with either identifier
|
|
resolves to one account — legacy duplicates are auto-merged.
|
|
- **BizGaze SSO** (`GET /sso`): an HMAC-SHA256-signed, time-boxed token (`SSO_SECRET`) signs an agent straight
|
|
in (auto-provisioning the user) and redirects to `/connect`.
|
|
- **Roles & tenancy:** every user belongs to a **team (tenant)**; roles are **`admin` / `technician` / `viewer`**
|
|
(default `technician`). Admins manage members (`/api/users`, `/api/users/manage`: add / reset-password /
|
|
rename / activate / deactivate / delete) and see tenant-wide views. Role is synced from BizGaze `isAdmin` each
|
|
login; `ADMIN_EMAILS` is a lockout safety-net.
|
|
- **Registration** (`POST /api/register`): first run creates the team + admin user; closed afterward unless
|
|
`ALLOW_REGISTRATION=1`.
|
|
- **Third-party API keys** (`api_keys`, `apiKeyFromReq`): `bzc_`-prefixed, stored hashed, comma-scoped (`*` =
|
|
all), presented via `X-API-Key` or `Authorization: Bearer bzc_…` — for the roadmap's 3rd-party integration.
|
|
- **Directory search** (`server/directory.js`, `/api/directory/search`): server-side BizGaze cross-tenant user
|
|
lookup; the directory auth token stays **server-side** and is never exposed to the browser.
|
|
- **Audit log** (`audit_log`): logins, SSO/BizGaze provisioning, account merges, and every admin action on a
|
|
member are recorded.
|
|
|
|
## Real-time & media
|
|
|
|
- **WebRTC** for calls/meetings: built-in **P2P mesh** by default; **LiveKit SFU** when configured.
|
|
- **LiveKit SFU server:** `livekit/livekit-server:**v1.9**` (Docker, `sfu` compose profile, dormant unless
|
|
`LIVEKIT_*` env is set). Server mints per-user JWT join tokens (`server/livekit.js`).
|
|
- **LiveKit clients:**
|
|
- Web: self-hosted **`livekit-client` ~2.20** UMD (`server/public/vendor/livekit-client.umd.min.js`).
|
|
- iOS: **LiveKit Swift SDK `2.15.3`** (SPM, `client-sdk-swift`, product `LiveKit`).
|
|
- Android: **LiveKit Android SDK** `io.livekit:livekit-android` **2.29.2** (+ `com.twilio:audioswitch` for
|
|
output routing, Kotlin coroutines) — native call **audio** + Speaker/Bluetooth/Earpiece (Increment 2a); native
|
|
**video** is Increment 2b. Needs the JitPack repo; raises the app to **minSdk 24**.
|
|
|
|
## Web client (`server/public/`)
|
|
|
|
- **Vanilla JavaScript SPA** — `home.html` is the app (large single file with inline JS). **No framework, no
|
|
bundler, no build step.** Supporting files: `icons.js` (shared Lucide icon set via `ic()`), `connect.html`
|
|
(embed), `sw.js` (service worker), `bizconnect-toast.js`.
|
|
- **PWA:** installable; service worker for Web Push + offline shell.
|
|
- **Icons:** Lucide (shared set, rendered through `ic()`); no emoji as control icons.
|
|
|
|
## Mobile app (`mobile/`) — Capacitor
|
|
|
|
- **Framework:** **Capacitor 8** (`@capacitor/core` `^8`) — a native shell (iOS WKWebView / Android WebView)
|
|
loading the remote web UI from `https://remote.bizgaze.com`. **Not Flutter/React Native.**
|
|
- **Official plugins (`^8`):** App, Camera, Filesystem, Keyboard, Share, Push Notifications, Splash Screen,
|
|
Status Bar; **community** `@capacitor-community/safe-area` `^8`; `@capacitor/assets` `^3` (icons/splash).
|
|
- **Local/custom plugins (`mobile/plugins/`):**
|
|
- `native-call` — native calling. **iOS:** Swift, CallKit + PushKit (VoIP) + LiveKit Swift SDK (native media,
|
|
video tiles, screen-share via a ReplayKit broadcast extension), on-device live transcription
|
|
(`SFSpeechRecognizer`). **Android:** Java, full-screen incoming-call UI over the lock screen + a
|
|
`FirebaseMessagingService` woken by high-priority FCM data messages. **Increment 2a** adds native LiveKit
|
|
**audio** (Kotlin `CallMedia` + a `microphone` foreground service for background audio + `AudioSwitchHandler`
|
|
routing), gated by the server flag `ANDROID_NATIVE_MEDIA`. **Increment 2b** adds native **video** (Kotlin
|
|
`CallVideo`: camera publish/flip + remote/local `TextureViewRenderer` tiles hole-punched behind a transparent
|
|
WebView), gated by `ANDROID_NATIVE_VIDEO`. **Increment 2c** adds **screen-share** (MediaProjection grant →
|
|
LiveKit `setScreenShareEnabled`; LiveKit's own `mediaProjection` foreground service) + **pinch-zoom** of a
|
|
received shared screen (web forwards the gesture under hole-punch → renderer transform).
|
|
- `audio-route` (iOS audio output routing), `media-library` (save to Photos/gallery), `share-inbox` (inbound
|
|
share sheet), `file-opener` (native Quick Look / open-with).
|
|
- **iOS build:** Capacitor 8 + **SPM** (CocoaPods retired); Xcode 26; a share extension + a broadcast extension.
|
|
- **Android build:** JDK **21**, AGP 8.7 / Gradle 8.11, **minSdk 24** (raised for LiveKit), target SDK 36,
|
|
edge-to-edge; the native-call plugin module also applies **kotlin-android** for the LiveKit media layer.
|
|
`mobile/android/` is generated fresh per build, so permissions, the **minSdk bump**, and the **JitPack
|
|
repository** (`allprojects`/`dependencyResolutionManagement`) that LiveKit Android's transitive
|
|
`com.github.davidliu:audioswitch` fork needs are all applied in CI by `mobile/scripts/android-patch.js`.
|
|
|
|
## Desktop app (`desktop/`) — Electron
|
|
|
|
- **Electron 31** + **electron-builder 24** (NSIS/Windows, DMG/mac, AppImage/Linux) + **electron-updater 6**
|
|
(auto-update from `https://remote.bizgaze.com/downloads/`). Wraps the web UI and adds native screen capture.
|
|
Optional **`@nut-tree-fork/nut-js`** for input automation.
|
|
|
|
## Native host agent (`agent/`) — Electron
|
|
|
|
- Legacy **remote-access agent** (`remote-access-agent`): Electron 31 host for screen capture, WebRTC,
|
|
consent, and OS input injection (`@nut-tree-fork/nut-js`), signaling over `ws`.
|
|
|
|
## Push notifications
|
|
|
|
- **Web Push** via VAPID (`web-push` `^3.6`) for browsers/PWA.
|
|
- **Android:** Firebase Cloud Messaging (FCM) — Firebase project `bizgaze-connect`; client `google-services.json`
|
|
ships in the APK; server uses a service-account key (base64 in `.env`). High-priority **data** messages wake the
|
|
native-call plugin for incoming calls.
|
|
- **iOS:** Apple Push Notification service (APNs) over HTTP/2 with a token (`.p8`) auth key, including **VoIP
|
|
pushes** (PushKit → CallKit) for incoming calls.
|
|
|
|
## Infrastructure & deployment
|
|
|
|
- **Containers:** Docker + Docker Compose (`Dockerfile`, `docker-compose.yml`). Services: `app` (Node),
|
|
`bizgazepg` (Postgres 16), `bizgazeredis` (Redis 7, `scale` profile), `livekit` (SFU, `sfu` profile).
|
|
- **Reverse proxy / TLS:** **Nginx Proxy Manager** (external, shared `nginx_proxy_manager_default` network) —
|
|
terminates HTTPS and proxies `remote.bizgaze.com` → `app:8090` and `wss://livekit.bizgaze.com` → LiveKit.
|
|
- **Persistence:** named Docker volumes — app data (`/data`: uploads, recordings, transcripts, downloads) and
|
|
Postgres data.
|
|
- **Deploy flow:** push to `master` (Gitea remote `code.bizgaze.com`) → `redeploy.sh` (plink/SSH to the host) →
|
|
server-side `deploy.sh` (git pull + docker rebuild + recreate). Secrets live in a gitignored `.env` on the
|
|
server (`/opt/bizgaze-support/.env`); see `.env.example`.
|
|
- **CI (mobile builds):** **Codemagic** (`codemagic.yaml`) — iOS (`mac_mini_m2`, SPM, publishes to TestFlight) and
|
|
Android (`mac_mini_m2`, Node 22, JDK 21, produces an installable debug APK).
|
|
|
|
## External integrations
|
|
|
|
- **BizGaze** SSO / identity provider + outbound webhook (`bizgaze.js`, `webhooks.js`).
|
|
- **SMTP email** via **Nodemailer** `^6.9` (optional) — meeting invites to external participants.
|
|
- **GIPHY** GIF search (server-proxied, optional, key-gated).
|
|
- **TURN/STUN** for WebRTC NAT traversal (credentials via `.env`).
|
|
|
|
## Dev & test tooling
|
|
|
|
- **Local DB for tests:** PostgreSQL 16 (local).
|
|
- **Browser automation:** `puppeteer-core` driving local Chrome (scratch testing).
|
|
- **Android debugging:** `adb` (`platform-tools`).
|
|
- **Shell/OS:** developed on Windows; Git Bash + PowerShell; deploy via plink (PuTTY).
|
|
|
|
---
|
|
|
|
## Repo layout (key directories)
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `server/` | Node backend (API, WS, data access, push, media) |
|
|
| `server/public/` | Vanilla-JS web UI (`home.html`, `icons.js`, `sw.js`, `vendor/`) |
|
|
| `server/db/` | PostgreSQL schema + DB adapter |
|
|
| `mobile/` | Capacitor app (iOS/Android) |
|
|
| `mobile/plugins/` | Custom Capacitor plugins (native-call, audio-route, media-library, share-inbox, file-opener) |
|
|
| `desktop/` | Electron technician desktop client |
|
|
| `agent/` | Legacy Electron remote-access host agent |
|
|
| `Dockerfile`, `docker-compose.yml`, `livekit.yaml` | Deploy stack |
|
|
| `codemagic.yaml` | Mobile CI (iOS + Android) |
|
|
| `redeploy.sh`, `deploy.sh` | Deployment |
|
|
| `ARCHITECTURE.md`, `DEPLOY.md`, `CLIENTS.md`, `README.md` | Docs |
|
|
|
|
---
|
|
|
|
## Maintaining this file
|
|
|
|
Update this document in the **same commit** as any change that alters the stack, specifically when you:
|
|
|
|
- add/remove a dependency, or bump a major version (server `package.json`, `mobile/package.json`,
|
|
`desktop/package.json`, `agent/package.json`, plugin manifests);
|
|
- change a platform/toolchain version (Node, Postgres, Redis, LiveKit, Capacitor, Electron, JDK, Xcode);
|
|
- add/remove a service in `docker-compose.yml` or a build workflow in `codemagic.yaml`;
|
|
- add a new custom Capacitor plugin or a new external integration.
|
|
|
|
Bump the **Last updated** date at the top and add a line to the Changelog below.
|
|
|
|
## Changelog
|
|
|
|
- **2026-11-03 (rev 6)** — Android native call **screen-share** (Increment 2c): `CallMedia.setScreenShareEnabled`
|
|
(MediaProjection → LiveKit; SDK-provided `mediaProjection` foreground service) + plugin `startScreenShare`/
|
|
`stopScreenShare` (permission via `startActivityForResult`), and **pinch-zoom** for a received shared screen
|
|
(`CallVideo.setTileZoom`). APK-only — no server/web change.
|
|
- **2026-11-03 (rev 5)** — Reliability: Postgres pool safety timeouts (`connectionTimeoutMillis`,
|
|
`statement_timeout`, `idle_in_transaction_session_timeout`, max 10→20) so a saturated pool fails fast instead
|
|
of hanging forever; and a **pre-deploy HTML syntax guard** (`server/tools/check-syntax.js`) run in the Docker
|
|
build and `redeploy.sh` — a JS syntax error in an inline `<script>` now fails the build instead of reaching
|
|
production (it had caused an app-wide login hang).
|
|
- **2026-11-03 (rev 4)** — Android native call **video** (Increment 2b): `CallVideo` renders camera/screen tiles
|
|
with LiveKit `TextureViewRenderer` hole-punched behind a transparent WebView; `CallMedia` gains camera
|
|
publish/flip + track lookup; new `ANDROID_NATIVE_VIDEO` server flag. Also fixed the white meeting background
|
|
(hole-punch no longer turns on without native video).
|
|
- **2026-10-30 (rev 3)** — Android native call media (Increment 2a): added `io.livekit:livekit-android` 2.29.2 +
|
|
`com.twilio:audioswitch` + Kotlin to the native-call plugin, a `microphone` foreground service, the
|
|
`ANDROID_NATIVE_MEDIA` server flag, and raised the Android app to minSdk 24. Build fix: `android-patch.js`
|
|
now injects the **JitPack** repo (LiveKit's `com.github.davidliu:audioswitch` fork resolves nowhere else).
|
|
- **2026-10-27 (rev 2)** — Added the **Identity & auth** section (sessions/cookies + bearer, refresh tokens,
|
|
TOTP MFA, BizGaze IdP/SSO, roles & tenancy, API keys, directory, audit).
|
|
- **2026-10-27** — Initial tech-stack document created.
|