Install Project Brain
Use this guide for a production deployment. For local development, see the README.
Project Brain is a single-tenant Docker Compose stack. Each customer runs their own deployment; there is no shared SaaS environment.
Requirements
- Linux VM: Ubuntu 22.04+, Debian 12+, RHEL 9+, or similar
- Public DNS name pointed at the VM, such as
brain.acme.com - Docker Engine 24+ with Compose v2
- OpenSSL
- An API key for Google Gemini, Anthropic Claude, or OpenAI
Open inbound 80/tcp and 443/tcp. Keep 5432, 6379, 3000, and 8080 closed; those ports are internal to Docker.
VM sizing
| Use case | Suggested VM | Notes |
|---|---|---|
| Trial or small team, up to 25 users | 4 vCPU / 8 GB RAM / 100 GB SSD | Good starting point |
| Production, up to 200 users | 8 vCPU / 16 GB RAM / 250 GB SSD | Enough room for web, worker, database, and sidecar |
| Heavy LLM usage | Production size plus 100 GB for /var/lib/docker/volumes | LLM cost usually exceeds VM cost |
Quick install
git clone https://github.com/Neusis-AI-Org/project-brain-releases.git
cd project-brain-releases
./scripts/install.sh
The installer asks for:
- App domain
- Super-admin email
- LLM provider
- Provider API key
It then writes .env, generates secrets, pulls images, runs migrations, starts the stack, and checks readiness.
The public release repository contains installer assets only. The Project Brain source repository remains private; customer deployments pull signed images from ghcr.io/neusis-ai-org.
When the installer reports readiness=true, open:
https://<your-domain>
Sign in with the super-admin email you entered during setup.
What gets installed
The installer creates:
.env, mode0600, with generated secrets and deployment config- App services:
web,worker,graphify-sidecar,graphify-egress-proxy,postgres,redis caddyfor TLS termination and automatic Let's Encrypt certificatespostgres-backupfor daily database backups in./backups/postgres/workspace-backupfor nightly workspace backups in./backups/workspace/- A super-admin account, promoted on first sign-in
The default KMS_PROVIDER=local keeps secrets in .env and encrypts stored credentials with BRAIN_MASTER_KEY. For a "no plaintext keys" deployment on AWS — KMS envelope encryption for credentials, a non-exportable KMS signing key for the GitHub App, and <NAME>_FILE secret injection from AWS Secrets Manager / Vault — set KMS_PROVIDER=aws and follow the AWS KMS section in the security model.
Configure SSO
Production requires a real identity provider. The installer leaves AUTH_AZURE_AD_* blank, and the app refuses to run production auth with DEV_AUTH_ENABLED=true.
A second flag, DEMO_AUTH_ENABLED=true, also enables the email-only credentials provider, and unlike DEV_AUTH_ENABLED it is honored even when NODE_ENV=production. It exists so prebuilt production images can be reused for public demo and staging deployments (where Next.js has already inlined NODE_ENV at build time). Never set DEMO_AUTH_ENABLED=true on a deployment with real customer data — it signs anyone in by email alone, with no password and no SSO. Leave it unset on customer installs.
To configure Microsoft Entra ID:
-
Register an OIDC application in your Entra tenant.
-
Add this redirect URI:
https://<your-domain>/api/auth/callback/microsoft-entra-id -
Create a client secret.
-
Update
.env:AUTH_AZURE_AD_TENANT_ID=...AUTH_AZURE_AD_CLIENT_ID=...AUTH_AZURE_AD_CLIENT_SECRET=...AUTH_ALLOWED_EMAIL_DOMAINS=acme.com -
Restart the web service:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d web
Configure the LLM provider
Project Brain uses an LLM for two independent workloads: the assistant chat (in-app Q&A grounded on the knowledge graph) and the graph builder (the graphify sidecar that extracts the knowledge graph). Each is configured separately, through two surfaces.
Bootstrap with environment variables
The installer writes a starting provider and key to .env:
LLM_PROVIDER=google # google | anthropic | openai
LLM_MODEL=gemini-3-flash-preview
GOOGLE_GENERATIVE_AI_API_KEY=...
# Optional custom / compatible endpoints:
LLM_BASE_URL= # applies to both workloads
GRAPH_BUILDER_BASE_URL= # graph builder only (e.g. an internal LLM gateway)
Until a workload is configured in the admin UI, both workloads run entirely from these env vars. GRAPH_BUILDER_BASE_URL overrides LLM_BASE_URL for the graph builder only, so graphify's egress can be pinned to an internal proxy without changing the assistant chat endpoint.
Override per workload in the admin UI
A super-admin can override the provider, model, endpoint, and key per workload on the LLM Settings page (super-admin only):
/admin/ai
For each of assistant_chat and graph_builder you can set:
- Provider —
google,anthropic, oropenai - Model — the model id for that provider
- Endpoint base URL (optional) — for Azure OpenAI, LiteLLM, a private gateway, or any OpenAI/Anthropic/Google-compatible endpoint
- API key — per provider, stored AES-256-GCM encrypted on
platform_ai_credentials(see Security model)
Precedence once a workload is saved:
- Provider and model come from the saved row —
LLM_PROVIDER/LLM_MODELno longer apply to that workload. - Base URL falls back to the matching env value only when left blank in the form.
- API keys are stored per provider (shared across workloads): a blank key field leaves any saved key in place, and the env key is used only when no key has ever been saved for that provider.
If you point the graph builder at a custom base URL (env or admin UI), its hostname must also be in GRAPHIFY_EGRESS_ALLOWED_HOSTS or the egress proxy blocks the call. See Graphify sidecar egress.
Connect integrations
Super-admins manage integrations at:
/admin/integrations
Each provider follows the same general shape: register with the provider, then complete the connect flow at /admin/integrations. GitHub uses an in-app manifest flow that registers the App for you; Atlassian uses an OAuth app you create manually first.
GitHub App
Project Brain registers its GitHub App through GitHub's manifest flow — you don't fill in permissions, URLs, or PEMs by hand. Credentials are stored encrypted on platform_integrations (AES-256-GCM via BRAIN_MASTER_KEY), not in .env.
Prerequisites
- A super_admin account in Project Brain (the first user with
INITIAL_SUPER_ADMIN_EMAILis auto-promoted on sign-in). - A publicly-reachable
APP_URL(e.g.https://brain.acme.corp). For local dev withAPP_URL=http://localhost:3000, the manifest flow works but the webhook URL is omitted automatically; wire it up once you have a public domain. - Admin rights on the GitHub org you want the App owned by — required to create an App under an org. Without org admin, register under your personal account first and transfer ownership later by re-registering.
Steps
- Open
/admin/integrationsand locate the GitHub card. - Type your GitHub org slug into the input (e.g.
acme-corp) and click Configure App. Leave blank to register under your personal account. - GitHub asks you to confirm the App name and permissions, then redirects back to Project Brain. The server exchanges the one-time
codefor the App's id, slug, client id/secret, webhook secret, and PEM private key — all encrypted and persisted. - The card now shows "app registered." Click Install on org to attach the App to the org whose repos Project Brain should access. GitHub redirects you back to
/api/github/setupand the installation id is captured automatically.
The manifest declares:
| Permission | Level |
|---|---|
| Contents | Read and write |
| Metadata | Read-only |
| Issues | Read-only |
Project Brain deliberately does not request the Administration permission. It never creates or deletes repositories — that stays under your own org's repo-provisioning governance. You create the repo on GitHub yourself, install the App on it, and then connect it from the in-app New project flow (which only needs Contents to commit into the existing repo). Deleting a project in Project Brain removes only its database rows; the GitHub repo is always left intact.
Each repo you connect must have a
mainbranch with at least one commit — the simplest way is to tick Add a README when creating it on GitHub. Project Brain commits tomainand can no longer auto-initialize an empty repo.
Switching org / re-registering
GitHub does not let you transfer App ownership. To move the App from your personal account to an org (or between orgs):
- Delete the App on GitHub: App settings → Advanced → Delete GitHub App.
- In Project Brain:
/admin/integrations→ Remove GitHub App (clears the encrypted credentials row). - Re-run the Configure App step, this time with the target org slug.
Switching the installation target
You can install one App on a different org without re-registering: click Reconnect on the card. GitHub redirects to the manage page for the current installation; from there you can uninstall and then reinstall on a different account via the same button. Note that today Project Brain tracks one installation per deployment — switching targets replaces, it doesn't add.
Atlassian
Jira and Confluence share one Atlassian OAuth app. The client id and client secret are stored encrypted on platform_oauth_apps (AES-256-GCM via BRAIN_MASTER_KEY), not in .env.
-
Sign in to Project Brain as a super_admin and open
/admin/integrations. The Atlassian OAuth card at the top shows the redirect URI to register on Atlassian — copy it. -
Create an OAuth app at https://developer.atlassian.com/console/myapps/.
-
Enable Authorization code grants (3LO).
-
Add scopes:
read:jira-workread:jira-userread:confluence-content.allread:confluence-space.summaryread:confluence-usersearch:confluenceoffline_access -
Paste the redirect URI from step 1 into the OAuth app's Callback URL field.
-
Back in Project Brain's Atlassian OAuth card, paste the OAuth app's client id and client secret, then Save Atlassian OAuth.
-
Click Connect Jira or Connect Confluence on the per-product cards. The standard three-leg OAuth flow runs and the resulting access/refresh tokens are stored encrypted on
platform_integrations.
To rotate credentials: click Remove Atlassian OAuth on the card, repeat steps 6 onward with the new values.
Verify the deployment
curl -fsS https://<your-domain>/api/health
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f web worker
readiness=true means the database is reachable and migrations are applied.
Point your monitoring tool at:
https://<your-domain>/api/health
Alert if readiness=false for two consecutive minutes.
Use your own TLS certificate
By default, Caddy gets a Let's Encrypt certificate with the HTTP-01 challenge. Use your own certificate when the deployment uses enterprise PKI, an internal CA, or cannot reach Let's Encrypt.
-
Place the certificate and key on the host:
./certs/fullchain.pem./certs/privkey.pemfullchain.pemmust include the leaf and intermediate certificates.privkey.pemmust be an unencrypted private key with mode0600. -
Set this in
.env:TLS_DIRECTIVE=tls /certs/fullchain.pem /certs/privkey.pem -
Restart Caddy:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d caddy
Caddy reloads certificates when the files change. APP_DOMAIN must match the certificate Common Name or SAN.
Air-gapped install
On a machine with internet access:
./scripts/airgap-bundle.sh v1.4.2
Move the generated archive to the air-gapped host, then run:
tar xzf project-brain-airgap-v1.4.2.tar.gz
cd project-brain-airgap-v1.4.2
docker load < images.tar
./scripts/install.sh
The bundle includes signed images, Compose files, install scripts, and SHA256SUMS.
Logs and audit events
web and worker write structured JSON logs to stdout. Each line includes service metadata, time, level, component, and event payload.
Set log level in .env:
LOG_LEVEL=info
Valid values are trace, debug, info, warn, error, and fatal.
Use debug briefly during an incident. Avoid trace in production unless you need very high-volume diagnostics.
The Docker json-file driver is the default log capture point. To ship logs to a SIEM, configure your Docker daemon or Compose override to use your platform driver, such as awslogs, gelf, syslog, fluentd, or splunk.
Privileged actions are recorded in two places:
- The
audit_logtable, visible at/admin/audit - Structured stdout logs with
kind: "audit"
Sensitive fields are redacted automatically, including password, authorization, cookie, token, apiKey, secret, clientSecret, accessToken, and refreshToken.
Add more keys with:
LOG_REDACT=header,xApiToken
Graphify sidecar egress
Knowledge graph builds run in graphify-sidecar, a non-root container that runs the Graphify CLI directly. The worker prepares the Git checkout and sends the sidecar only a prepared workspace path plus model settings; GitHub installation tokens stay in the worker.
The sidecar is attached only to an internal Compose network. Its outbound LLM access goes through graphify-egress-proxy, which allows only configured destination hosts:
GRAPHIFY_SIDECAR_TOKEN=<32+ random chars>
GRAPHIFY_EGRESS_ALLOWED_HOSTS=generativelanguage.googleapis.com,api.openai.com,api.anthropic.com
GRAPHIFY_SIDECAR_TOKEN is required: the worker refuses to dispatch a build without it and the sidecar rejects every request when it is unset. Generate it with openssl rand -hex 32 and keep it distinct from INTERNAL_SERVICE_TOKEN.
GRAPHIFY_EGRESS_ALLOWED_HOSTS entries must be valid hostnames; the proxy validates each one at startup and exits if any entry is malformed. Subdomain matching uses a leading dot (e.g. .acme.com) — wildcards like *.acme.com are not supported by the proxy ACL.
Enterprise deployments should replace the default allowlist with their approved LLM gateway or private model endpoint, for example:
GRAPH_BUILDER_BASE_URL=https://llm.internal.acme.com/v1
GRAPHIFY_EGRESS_ALLOWED_HOSTS=llm.internal.acme.com
Verify the boundary after install:
docker compose exec graphify-sidecar python -c "import urllib.request; urllib.request.urlopen('https://github.com', timeout=3)"
# expected: fails
docker compose exec graphify-sidecar python -c "import os,urllib.request; proxy=os.environ['HTTPS_PROXY']; print(proxy)"
Knowledge graph builds
After every accepted change to a project's repo — and on a manual or scheduled rebuild — the worker hands the workspace to graphify-sidecar, which runs graphify extract to (re)build the knowledge graph.
graphify extract is incremental: it keeps a SHA-256 content-hash cache in graphify-out/cache/, so unchanged files cost no LLM calls. Changed files are re-extracted at LLM cost, so every commit can trigger paid LLM usage for the files it touches. Two best-effort post-steps then regenerate the human- and assistant-facing artifacts:
| Artifact | Produced by | Purpose |
|---|---|---|
graphify-out/graph.json | graphify extract | The extracted knowledge graph (nodes + edges) |
graphify-out/GRAPH_REPORT.md, graph.html | graphify cluster-only | Human-readable community report + interactive view |
graphify-out/wiki/ (entry wiki/index.md) | graphify export wiki | The assistant's primary grounding source |
The post-steps are budget-gated and skipped if extraction already used its time budget; older builds may have only GRAPH_REPORT.md and no wiki/.
Tuning knobs are read by the sidecar from its own service environment (set them on the graphify-sidecar service):
GRAPHIFY_TIMEOUT_S=1800 # overall extraction budget (default 30 min)
GRAPHIFY_CLUSTER_ONLY_TIMEOUT_S=180 # GRAPH_REPORT.md + graph.html regeneration
GRAPHIFY_WIKI_TIMEOUT_S=120 # wiki export
GRAPHIFY_MAX_OUTPUT_TOKENS=32768 # per-call output token cap
Forcing a full rebuild
The graph rebuilds incrementally by default. If the cache becomes stale or corrupt, force a full rebuild from the project UI: hold Shift while clicking the Rebuild button. This wipes graphify-out/cache/ and re-extracts every file from scratch — at full LLM cost — so use it only for recovery, not routinely. The equivalent API call is:
POST /api/projects/<slug>/graph/rebuild?force=full
A forced rebuild only clears the extraction cache; the existing graph.json and GRAPH_REPORT.md stay in place until the new build overwrites them.