Product guides and code maps.View source

01 · Stack, configuration and development

The root README owns the first-run steps. .env.example, infra/deploy/ensemble.env.example, package manifests and workflow files own exact configuration, versions and commands. Deployment credentials and operator runbooks stay in the git-ignored private/ folder.

1. Stack at a glance

Layer Implementation
Hub Next.js 15 App Router, React 19, TypeScript, Tailwind, React Query
Editors and views TipTap/ProseMirror, CodeMirror, React Flow (@xyflow/react), ECharts
API Node 22+, Fastify, Zod, Prisma
Data (dev and hosted) PostgreSQL 16 with vector, pg_trgm, uuid-ossp; Redis 7 for transient state, rate limits, pub/sub
Durable jobs WorkspaceJob rows; in-process claim, worker and scheduler in hub-api
Data (desktop and CLI) Local Node API with PGlite and an in-memory Redis stand-in (lib/memory-redis.ts); no Docker
Model transport Python 3.11+ FastAPI runtime for dev and hosted; TypeScript adapters in the local sidecar (apps/hub-api/src/runtime)
File originals Postgres rows, or a private Azure Blob container with ENSEMBLE_FILE_STORAGE=azure-blob (lib/blob-store.ts)
Plot computation Browser runtime on the web; Python/server and native workers elsewhere
Desktop and quick capture Tauri 2 (Rust)
Editor context MCP SDK server over stdio/HTTP; hosted /mcp reads, and writes with a key that allows changes
Realtime Scoped SSE; page co-editing has its own version protocol
Logging Pino (API), structlog (Python), plus the app's audit ledger

2. Environment and tenancy

Accounts and spaces. Content is space-scoped. Login sessions, connector grants, model keys, paired devices and sharing permissions are separate concerns (28, 29). Hosted model, connector and agent calls need a verified email. Only verified accounts listed exactly in ENSEMBLE_OPERATOR_EMAILS may use host credentials or host execution; public deployments keep ENSEMBLE_SERVER_RUNNER=off, so ordinary accounts run code on paired computers. Production refuses to start with ENSEMBLE_DEV_AUTH_BYPASS on.

Connector OAuth. Login apps (AUTH_*) identify a person; connector apps authorize product access. A provider uses <PROVIDER>_CLIENT_ID/_SECRET, a saved encrypted operator app, or the matching sign-in app. Native callbacks are HUB_API_PUBLIC_URL/api/connectors/<provider>/callback (connectors/oauth.ts); vendor MCP has its own callback. connectors/products.ts owns the Google and Microsoft scopes: read-only mail, opt-in calendar write and availability, supported Drive and Office access (03 §2.1).

Content. Connecting saves a grant, not content. Sync people reads bounded contact metadata with no model call; search, attach, import and upload bring content in; scheduled fetching reads nothing (31).

3. Local development setup

Prerequisites: Node 22+, the pnpm version pinned in package.json, Docker with Compose, Python 3.11+.

cp .env.example .env
pnpm install
docker volume create ensemble_pg
pnpm infra:up
pnpm db:migrate
pnpm dev
  • The ensemble_pg volume persists data. pnpm db:migrate (apps/hub-api/scripts/migrate-deploy.mjs) runs prisma migrate deploy; a database made with pnpm db:push is aligned in place and its migrations recorded, without deleting rows. Never run prisma migrate reset.
  • scripts/dev.mjs starts web :3000, API :4000 and agent :5055. scripts/agent-dev.sh creates apps/agent-runtime/.venv on first use. Run one agent runtime; pnpm dev:agent starts only that component. Port 5055 avoids macOS's use of 5000.
  • pnpm run doctor checks this repo's environment (pnpm doctor is pnpm's own command).

Configuration groups

.env.example (development) and infra/deploy/ensemble.env.example (servers) are authoritative and commented.

Group Examples
Database and origins DATABASE_URL, REDIS_URL, HUB_API_HOST, HUB_API_PORT, HUB_WEB_ORIGIN, HUB_API_PUBLIC_URL, NEXT_PUBLIC_HUB_API, ENSEMBLE_COOKIE_DOMAIN, ENSEMBLE_COOKIE_NAMESPACE (identical on API/web; unset for default cookies), ENSEMBLE_NODE_WEB_ORIGIN (the Ensemble Node website, 35)
Ensemble Node ENSEMBLE_NODE (off stops node turns and run reports), ENSEMBLE_NODE_PARALLEL (node turns at once per API process, default 4) (35)
Signup and identity ENSEMBLE_SIGNUP_MODE, ENSEMBLE_SIGNUP_ALLOWLIST, AUTH_GOOGLE_*, AUTH_GITHUB_*, AUTH_MICROSOFT_*, EMAIL_PROVIDER (Resend), Turnstile keys
MCP publishing OPENAI_APPS_CHALLENGE (optional one-line plaintext proof at /.well-known/openai-apps-challenge; unset returns 404). Hosted OAuth uses the API origin and existing auth infrastructure (Context Bridge §4.1)
Host restrictions ENSEMBLE_OPERATOR_EMAILS, ENSEMBLE_SERVER_RUNNER, ENSEMBLE_TERMINAL; dev bypass and ENSEMBLE_DEV_TOOLS for isolated local work only
Secrets ENSEMBLE_INTERNAL_TOKEN, ENSEMBLE_BRIDGE_TOKEN, ENSEMBLE_SECRET_KEY (or the local secret-key file)
Model providers Provider keys, endpoints and role/model settings; hosted ordinary users never use operator keys
Connector apps Provider client variables, Google Picker settings, vendor MCP clients (03)
Included AI AZURE_FOUNDRY_RESOURCE, AZURE_FOUNDRY_API_KEY, ENSEMBLE_INCLUDED_MODELS, ENSEMBLE_INCLUDED_WEB_SEARCH, ENSEMBLE_INCLUDED_MONTHLY_CREDITS, ENSEMBLE_INCLUDED_TOTAL_MONTHLY_USD, ENSEMBLE_INCLUDED_MAX_OUTPUT_TOKENS (33)
Model calls ENSEMBLE_MODEL_CALL_TIMEOUT_MS: how long one model call may take, 900000 (15 minutes) by default, 30 s to 30 min; hub-api and agent-runtime read it (11 §5)
Workspace and local runtime ENSEMBLE_WORKSPACE_ROOT, ENSEMBLE_DATA_DIR, state directories, queue/remote-loop/scheduler switches
Agent sessions ENSEMBLE_SESSION_CHOICE_CONFIG: a JSON file overriding the weights, bands and limits a computer chooses a run's agent session with; the server sends it with every claim (30, Choosing a session)
Hosted quotas ENSEMBLE_MAX_CONNECTORS, ENSEMBLE_MAX_JOBS_PER_DAY, ENSEMBLE_MAX_DATASET_BYTES, ENSEMBLE_MAX_DOCUMENTS, ENSEMBLE_MAX_DOCUMENT_BYTES, ENSEMBLE_FILES_TOTAL_MAX_BYTES
Server storage and monitoring ENSEMBLE_FILE_STORAGE, AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_CONTAINER, ENSEMBLE_BACKUP_CONTAINER, ENSEMBLE_METRICS_PORT, ENSEMBLE_INTERACTION_RETENTION_DAYS
Extraction and plots Enrichment switch, file-store ceiling, plot worker concurrency and memory (32, 20)
Diagnostics ENSEMBLE_LOG_LEVEL, readiness and rate-limit settings

.env.example also lists ENSEMBLE_LOG_TO_FILE, ENSEMBLE_LOG_RETENTION_DAYS and ENSEMBLE_FEATURES; no code reads them. Modules are gated by packages/shared-types/src/modules.ts and lib/module-gate.ts.

4. Libraries and runtime ownership

Manifests and lockfiles hold exact versions. Beyond §1: the MCP SDK, SimpleWebAuthn, fflate, Office/CSV readers and writers, pdfjs-dist, @azure/storage-blob, prom-client, PGlite; jspdf serves both the browser and the assistant's server PDF export. Python holds FastAPI, httpx, provider adapters and the plotting stack. Either runtime can resolve encrypted provider keys. packages/prompts and apps/skill-forge are stubs.

5. Data storage (Postgres)

One Prisma migration history manages all tables in the public schema. Vector and tsvector columns exist but nothing fills or queries them; an embedding key enables nothing.

lib/ledger.ts writes a per-space hash-linked history. No INSERT-only database role protects it, and retention and deletion can remove rows. Its verification checks link continuity, not full content hashes (07).

SQL account-lifetime constraints differ from Prisma's relation model on purpose. New migrations must keep them; rely on the schema-drift tests rather than generated DROP statements (00 §7).

6. Model usage policy

6.1 Profiles and discovery

Low (easy), Medium, High and Max profiles each hold {provider, model, effort}; defaults are in packages/shared-types/src/assistant.ts. Hosted defaults can pick included models when the configured provider has no usable key.

  • GET /api/models reads cached discovery (lib/model-catalog.ts); POST /api/models/refresh refreshes without inference. A catalog refreshes after a week, sooner after an error, and when its key changes.
  • A model chosen without its provider must match a tier's model or carry an unmistakable vendor prefix; otherwise it is refused (lib/model-choice.ts).
  • Thinking levels come from packages/shared-types/src/model-efforts.ts and lib/model-meta.ts. default sends no level; an unsupported effort is rejected before the call. Context-window metadata is informational.

6.2 Callers and credentials

Tasks pin their profile when assigned. The assistant, inline asks, briefs and enrichment resolve their saved profile or role. A partial settings patch changes only the submitted fields.

Personal API keys, local subscriptions and included models are separate paths. Subscription calls run on the selected paired computer; external engines use their own sign-in. Hosted ordinary users never fall back to the operator's environment, CLI login or host Ollama. Copilot's HTTP adapter and a Copilot CLI handoff are different integrations.

6.3 Usage and estimates

Run steps and model metrics keep the reported model and token counts. Dollar and credit estimates stay estimates; unknown rates are not treated as free. Recorded models are not rewritten when Settings change. Included credits use real reservation and settlement.

6.4 Included AI

The ensemble catalog defaults to GPT-5 mini and GPT-4.1 mini; each deployment must exist and have quota. The hosted personal dev/test setup uses Visual Studio Azure credits; Luna, Sol and Grok are not currently included, but remain supported for explicit eligible deployments. Each account gets 20,000 credits per UTC month, bounded by its environment's operator ceiling and a worst-case reservation. Azure hosted grounding is opt-in because it can incur separate charges. A paired desktop or CLI uses the hosted catalog without receiving Azure credentials (33).

7. Conventions and logs

  • TypeScript is strict; wire shapes use shared Zod schemas. pnpm typecheck also runs pnpm version:check. There is no repository-wide ESLint, Prettier, ruff or mypy gate.
  • Writes go through the owning service's transaction, authorization, ledger and Undo paths. The Python tools/guarded.py policy covers only its own callers. Follow AGENTS.
  • Logs go to the console; servers use the journal (journalctl --user -u ensemble-api on the home server, journalctl -u ensemble-api on a VM). Redaction helpers (lib/redact.ts, ensemble_agent/redact.py) cover known secret patterns only. Keep credentials and tokens out of prompts and logs.

8. CI and deploys

Workflow files are authoritative for pinned action and runtime versions.

.github/workflows/ci.yml runs on pull requests except those targeting testing (a feature branch or testing into main), and every push to main. Its one job, Typecheck and unit tests, runs the fast checks shared with testing (.github/actions/fast-checks): pnpm version:check, pnpm db:generate, the shared-types, hub-api, hub-web and CLI typechecks, CLI channel isolation, focused web and in-memory API regressions, included mini-default/credit-ledger/grounding-opt-in regressions, the testing deploy guard, the release-script tests and the route inventory. It is the required status check on main and production. The slower suites (the full hub-api suite against Postgres and Redis, every package's tests, sandbox escapes on four OSes, agent-runtime tests, production builds) are paused in full-checks.yml, run by hand (37).

.github/workflows/deploy.yml runs after ci succeeds on a push to main, or by hand (Actions → deploy → Run workflow: all, api, app or landing). It does nothing unless the repository variable DEPLOY_ENABLED is true. One deploy runs at a time.

  1. Promote. If the commit is still the tip of main, the workflow fast-forwards the production branch to it (never a force push).
  2. API. The API server pulls; nothing connects in. autodeploy.sh, run every minute by ensemble-autodeploy.timer, fetches origin/$ENSEMBLE_DEPLOY_BRANCH and runs update.sh when it moved. update.sh writes ENSEMBLE_COMMIT=<sha> to the commit file, which hub-api reports as commit on GET /health. The workflow waits up to 25 minutes for /health to report the commit and /health/ready to return 200. A commit whose update fails is recorded in autodeploy.failed and not retried until a newer commit arrives.
  3. Hub and landing page. .github/actions/vercel-deploy runs vercel pull, vercel build --prod and vercel deploy --prebuilt --prod for each Vercel project, after deleting .git (Vercel's Hobby plan refuses a deploy whose commit author is not the account owner). The build retries once after 20 seconds for transient font-fetch failures; a second failure stops the publish. The Hub waits for the API step; the landing page does not.
  4. Search engines. After the landing page publishes, apps/landing/scripts/indexnow.mjs submits the live sitemap.xml to IndexNow (Bing, and through it DuckDuckGo, Yahoo and Ecosia; also Yandex, Seznam and Naver). The key is public: apps/landing/public/<key>.txt and the script's KEY constant must match. The step is continue-on-error. Google reads the sitemap through Search Console instead.

Repository secrets: ENSEMBLE_API_URL, VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID_APP, VERCEL_PROJECT_ID_LANDING. The Vercel projects hold build settings (root apps/hub-web or apps/landing) and the Hub's NEXT_PUBLIC_* values. Server secrets stay in the server's env file and never go to GitHub or Vercel.

Testing uses test-web.yml on pushes and PRs to testing: the same fast checks as ci.yml (.github/actions/fast-checks), including morning-brief routine migration, signup, scheduling and UI regressions. Successful pushes advance testing-live, wait for the isolated API's exact commit and readiness, then deploy the separate test Vercel project. After publication, reusable test-cli.yml packages and smoke-tests ensemble-test on five platform/architecture targets, then publishes the separate cli-testing-latest prerelease only if its source still matches both testing pointers. The web deployment does not wait for package builds. TEST_DEPLOY_ENABLED, ENSEMBLE_TEST_API_URL and VERCEL_PROJECT_ID_TEST control it; production CLI/native releases are unchanged. infra/deploy/testing owns the isolated backend and refuses production resources. Installation, purpose and promotion: 36.

Public documentation uses apps/docs to export tracked docs/ to apps/docs/out for the separate Cloudflare hostname docs.ensemblework.com. The shared fast checks build the site and run docs rendering/search, bridge-tool and hosted-auth regressions. They do not publish the docs or change DNS. Build output, hostname attachment and the public index read by ensemble_docs: 40.

The shared fast checks include hosted MCP OAuth discovery, browser consent, code/client/resource binding, PKCE, refresh/replay/revocation, named tool dispatch and legacy compatibility. They also check provider sign-in returns, the consent UI and its public middleware route, the page/route inventory, and Grok Build's native MCP setup commands. The fixtures are in-memory or local SDK clients, not paid model or real vendor-directory tests (Context Bridge).

The fast checks also cover readable checkout naming (workspace/checkout-folder.test.ts): selected-model requests, safe title fallback, concurrent reservations, name collisions, continuation, task labels and folder grants. They cover confirmed session deletion: hosted stop/acknowledgement/retry and account isolation, local native-history propagation, exact-file preservation and continuation gating. Native fixture tests use disposable history, not real agent accounts or model calls.

Active-place and conditional-save regressions run in the same fast action: three-place admission, expiry, release and replacement, key exemption, row-locked field merges and conflict resolution, plus the Hub chooser/dialog. API fixtures use migrated in-memory PGlite; those files run serially.

Home server (infra/deploy/home) — serves api.ensemblework.com

  • One machine, run as its own user without root. setup.sh installs it (safe to rerun); everything lives in ~/ensemble-host: app/ checkout, ensemble.env (mode 0600, from the "Home server" block of infra/deploy/ensemble.env.example), ensemble.commit, node/, cloudflared/, state/ (including autodeploy.failed), backups/, logs/, bin/.
  • Postgres (pgvector/pgvector:pg16), Redis and the Cloudflare Tunnel are Docker containers from compose.yml with restart: unless-stopped; Postgres and Redis listen on loopback ports 15432 and 16379, and the API and agent on 14000 and 15055, so pnpm dev can run on the same machine. The tunnel dials out, so the machine needs no open port, router rule or fixed IP.
  • The API, agent, ensemble-autodeploy.timer and ensemble-pg-backup.timer are systemd user units; lingering starts them at boot.
  • Cron runs watchdog.sh every 15 minutes and once after boot. It starts stopped containers and units, restarts the agent and API when loopback /health/ready fails for a minute, and restarts the tunnel when the public /health fails while loopback is ready.
  • pg-backup.sh runs nightly: a pg_dump plus a tarball of on-disk state, seven kept locally, each copied to the ensemble-backups blob container (ENSEMBLE_BACKUP_CONTAINER). File originals and plot tables already in blob storage are not copied again. update.sh takes a local-only dump (--local) before every migration and stops the deploy if it fails. ENSEMBLE_SECRET_KEY is not in backups.
  • File originals and plot tables are stored in a private Azure Blob container (ENSEMBLE_FILE_STORAGE=azure-blob, 32 §2).
  • Monitoring (infra/deploy/home/observability, optional, observability/setup.sh): Prometheus, Grafana, node-exporter and postgres-exporter in Docker on loopback, hub-api metrics on ENSEMBLE_METRICS_PORT, an Azure cost and storage timer, and Grafana on Tailscale devices through tailscale serve. Once installed (~/ensemble-host/observability.env exists), update.sh drops and recreates the analytics views around migrations and rebuilds the dashboards, and the watchdog keeps the containers and timer running (07 §1.3).

VM install (infra/deploy)

An alternative install of the same pull loop on Ubuntu 24.04 with root: setup.sh /etc/ensemble.env, Caddy on 443, system units, /etc/ensemble.env, /etc/ensemble.commit and /var/lib/ensemble/autodeploy.failed. Its logs are under sudo journalctl -u ensemble-autodeploy.

Other workflows

  • .github/workflows/desktop.yml builds and publishes desktop installers on a desktop-v* tag, a push to the desktop branch, or by hand; it runs no tests and not on pull requests (DESKTOP, 37).
  • .github/workflows/full-checks.yml runs the paused suites by hand (Actions → full-checks → Run workflow; 37). Its Export the desktop UI step retries once, because the Google Fonts download sometimes fails on hosted runners.
  • .github/workflows/cli-release.yml runs on a cli-v<version> tag (matching apps/cli/package.json) or by hand as a dry run. It builds and smoke-tests the CLI on macOS (arm64, Intel), Linux (x64, arm64) and Windows, builds .deb/.rpm, publishes a GitHub release with --latest=false, and pushes the Homebrew formula and Scoop manifest. Secrets: HOMEBREW_TAP_DEPLOY_KEY, SCOOP_BUCKET_DEPLOY_KEY (write deploy keys on ensemblework/homebrew-tap and ensemblework/scoop-bucket), optional WINGET_TOKEN and NPM_TOKEN; a job whose secret is missing is skipped with a notice (26 §7).

9. API regression tests

pnpm --filter @ensemble/hub-api test:public
pnpm --filter @ensemble/hub-api route:inventory
pnpm --filter @ensemble/hub-api route:inventory:check
  • src/app.ts builds the production HTTP app without listeners, schedulers, queues or signal handlers. src/test/http.ts creates real browser sessions and scoped API tokens against migrated in-memory PGlite; no Docker or model key is needed, and missing critical infrastructure fails instead of skipping. ENSEMBLE_TEST_DATABASE_URL may name a migrated, dedicated PostgreSQL database whose name contains test.
  • test:public runs inside the normal hub-api test: its files in parallel, then src/node/takeover.integration.test.ts on its own (its 150 ms key latency check is measured without the other files' browsers and databases running), then route:inventory:check. It covers route gates; signup, recovery and OAuth; resource isolation; hosted execution and quotas; file erasure; provider-compatible tool schemas. OAuth, JWKS, email, Turnstile and model calls are mocked or locally signed.
  • apps/hub-api/scripts/route-inventory.ts writes the hosted and desktop route matrices to scripts/route-inventory.json; src/test/route-coverage.ts lists explicit request contracts. --check fails on a stale inventory; missing happy-path contracts only warn unless --strict.
  • Work-folder fixtures use src/test/temporary.ts (/private/tmp on macOS). The device-revocation cross-process test needs a real Redis and skips on memory://. Some hub-web tests hold the process open after their assertions because of query-cache timers.
  • pnpm --filter @ensemble/hub-web test first runs e2e/assistant-tile-catalog.ts --check, which keeps the assistant's generated tile metadata in line with the UI. apps/hub-api/scripts/assistant-eval.ts runs opt-in real-model checks over synthetic in-memory workspaces and reports token and cache usage; it is not in CI and needs a configured model transport (34).

10. Brand, versions and releases

  • pnpm brand:build (scripts/build-ensemble-brand.mjs, Sharp and esbuild) generates icons, native motion bundles and fallback pages from packages/brand into the apps and design/. pnpm brand:check verifies the generated manifest; node scripts/check-icons.mjs checks ICO/ICNS containers. In the CLI, ENSEMBLE_REDUCE_MOTION=1 turns off the brand animation and NO_COLOR controls color (BRAND).
  • Root release.json owns the version. pnpm version:sync (scripts/release-version.mjs --sync) writes package manifests, Tauri/Cargo configs and locks, Python metadata (ensemble_agent/version.py) and the shared TypeScript constant; pnpm version:check rejects differences. Web, API and landing prebuilds, CLI/sidecar packaging and native builds check it. /health, public /api/version, the Context Bridge server info and the Python runtime report it. GET /api/version?updates=1 checks published CLI and desktop channels, cached five minutes (30 seconds after a failure).
  • Desktop releases need signed updater packages for every platform (TAURI_SIGNING_PRIVATE_KEY is a GitHub Actions secret); OS code signing is not configured. Build matrix, packaging and recovery publishing are in DESKTOP.
  • Packaged launchers set ENSEMBLE_INSTALL_PATH internally; the remote loop stops and unpairs a runtime whose launcher stayed removed for 10 minutes (a package upgrade removes it only for a moment). It is not a setup variable.