Production runbook for the CodeGraph product app.
- Node ≥ 22 (uses the built-in
node:sqlite— no native modules). - git on PATH at runtime (clones public repos).
- Writable
./datadirectory (SQLite + WAL).
cd CodeGraph/app
npm install
npm run dev # http://localhost:4000npm run build # emits .next/standalone/server.js (output: "standalone")
npm run start # next start -p 4000cd CodeGraph/app
docker compose up --build # http://localhost:4000- Multi-stage build on
node:24-slim; runtime image includesgit. - Runs as root — deliberately; see
docs/postmortems/2026-07-10-render-crash-loop.md. A platform-mounted persistent disk (Render) doesn't inherit the image's baked-in ownership and can reset to root on every restart; dropping privileges after fixing ownership (setpriv) crash-looped because Render doesn't grantCAP_SETUID. Running as root sidesteps the whole class of problem. - SQLite persists to the named volume
codegraph-data(/app/data). HEALTHCHECKpolls/api/health.
| Var | Where | Default | Purpose |
|---|---|---|---|
NEXT_PUBLIC_APP_URL |
website build + GitHub OAuth callback | https://app.codegraph.dev |
Marketing "Start Indexing" target; also the base URL used to build the OAuth redirect_uri (falls back to the request's own origin if unset) |
PORT |
app runtime | 4000 |
HTTP port |
HOSTNAME |
app runtime | 0.0.0.0 |
Bind address (Docker) |
CG_MAX_FILES |
app runtime | 4000 |
Max files scanned per repo |
CG_CLONE_TIMEOUT_MS |
app runtime | 90000 |
git clone timeout |
CG_DATA_DIR |
app runtime | ./data |
SQLite location |
CG_ALLOW_LOCAL_ACCESS |
app runtime | unset (= off in production) | Opt in to local-folder indexing + server-side folder browsing on a public deployment. Only set true on a trusted, single-operator host. |
CG_BASIC_AUTH_PASSWORD |
app runtime | unset (= off) | Gate the whole app behind HTTP Basic Auth. Pairs with CG_BASIC_AUTH_USER (default codegraph). |
GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET |
app runtime | unset (= GitHub sign-in hidden) | From a GitHub OAuth App — see below. |
CG_SESSION_SECRET |
app runtime | unset (= GitHub sign-in disabled) | Encrypts the GitHub session cookie (AES-256-GCM). Required alongside the two vars above for GitHub sign-in to activate. Any long random string; rotating it signs everyone out. |
CG_OWNER_GITHUB_LOGIN |
app runtime | unset (= no owner-lock) | Restrict the entire app — every page and API route, including the normally-anonymous public bucket — to one or more GitHub logins (comma-separated, case-insensitive). Requires GitHub sign-in to be fully configured too (fails closed with a 500 otherwise). See below. |
ANTHROPIC_API_KEY |
app runtime | unset (= Claude AI Assistant hidden) | Enables the Editor tab's AI Assistant using Claude (Claude Agent SDK) — see below. Core product features never need this. |
CLAUDE_CODE_OAUTH_TOKEN |
app runtime | unset | Alternative to ANTHROPIC_API_KEY: authenticates Claude via a Pro/Max/Team subscription login instead of pay-per-token API billing. The only way subscription mode can work on a hosted/container deployment (no persistent home directory, no interactive terminal for claude login) — see below. |
CG_LOCAL_LLM_BASE_URL |
app runtime | unset (= local-model AI Assistant hidden) | Base URL of an OpenAI-compatible chat-completions endpoint (Ollama, LM Studio, llama.cpp server, vLLM, …), e.g. http://localhost:11434/v1. Required alongside CG_LOCAL_LLM_MODEL — see below. |
CG_LOCAL_LLM_MODEL |
app runtime | unset | Model name/tag to request from that endpoint, e.g. qwen2.5-coder:7b. Required alongside CG_LOCAL_LLM_BASE_URL. |
CG_LOCAL_LLM_API_KEY |
app runtime | unset (sends local) |
Optional bearer token if your local server's endpoint requires one; most (Ollama, LM Studio) don't. |
CG_LOCAL_ACCESS_ROOT |
app runtime | unset (= unrestricted once CG_ALLOW_LOCAL_ACCESS is on) |
Defense-in-depth: when set, confines /api/browse and local-folder indexing to descendants of this directory even if CG_ALLOW_LOCAL_ACCESS=true, so a local-access misconfiguration can't expose the whole filesystem. |
CG_FORCE_SECURE_COOKIES |
app runtime | unset (= derived from the request) | Override the session/OAuth cookies' Secure flag. Normally derived from x-forwarded-proto/the request's own scheme, not NODE_ENV — only set this if your proxy doesn't forward that header reliably. |
Lets a signed-in user browse and one-click import their own repos — including private ones — instead of pasting a URL. Fully optional and off by default; skip this section if you don't need it.
- Register a GitHub OAuth App: GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
- Homepage URL: your deployment's public URL (e.g.
https://codegraph-8qqc.onrender.com). - Authorization callback URL: that same URL +
/api/auth/github/callback(e.g.https://codegraph-8qqc.onrender.com/api/auth/github/callback) — must match exactly, including scheme. - For local dev too, either register a second OAuth App with callback
http://localhost:4000/api/auth/github/callback, or just reuse one app and update its callback URL when switching between local and deployed testing.
- Homepage URL: your deployment's public URL (e.g.
- Copy the Client ID, and generate + copy a Client Secret.
- Set three env vars on the deployment (and/or in a local
.env.localfor dev):GITHUB_OAUTH_CLIENT_ID=<from step 2> GITHUB_OAUTH_CLIENT_SECRET=<from step 2> CG_SESSION_SECRET=<any long random string, e.g. `openssl rand -hex 32`> NEXT_PUBLIC_APP_URL=<your deployment's public URL, no trailing slash>
- Restart/redeploy. A "Sign in with GitHub" link appears in the header and a "My GitHub" tab appears on the Start Indexing page automatically once all three of
GITHUB_OAUTH_CLIENT_ID/GITHUB_OAUTH_CLIENT_SECRET/CG_SESSION_SECRETare set — no other config needed, and nothing changes for anyone if you leave them unset.
By default, once GitHub sign-in is configured, repos indexed anonymously still land in a shared public bucket anyone can see/index into — sign-in only makes your own repos private. To instead make the entire deployment (every page, every API route) usable only by you:
- Complete the GitHub sign-in setup above first (
CG_OWNER_GITHUB_LOGINfails closed with a 500 if OAuth isn't fully configured — it never silently opens the app back up). - Set
CG_OWNER_GITHUB_LOGIN=<your-github-username>(comma-separate multiple logins for a small team, e.g.alice,bob). - Restart/redeploy. Now:
- An anonymous visitor hitting any page is redirected to GitHub sign-in; any API route returns
401. - A visitor signed in with a GitHub account not on the allowlist gets a static "Access Restricted" page (pages) or a
403(API) — never a redirect loop. GET /api/healthand the GitHub OAuth routes themselves (/api/auth/github*) always stay reachable, so health checks and the sign-in flow that satisfies this very check never get blocked by it.
- An anonymous visitor hitting any page is redirected to GitHub sign-in; any API route returns
- This is enforced once, in
src/proxy.ts(Next.js's middleware entry point) — no individual route needed changes, and it applies uniformly to pages,/api/repos,/api/index,/api/settings/*, the AI Assistant routes, everything.
What this grants: the OAuth scope requested is repo read:user — GitHub's classic OAuth has no finer-grained read-only scope, so this is full read/write access to the signed-in user's repos (needed to clone private ones at all) plus their public profile. The token is held only in an encrypted, httpOnly session cookie — never written to disk, never returned in any API response, and only ever sent back to github.com itself (see lib/session.ts and the runJob host check in lib/store.ts).
Adds a chat panel to the Editor tab. Either backend, both, or neither may be configured; if both are, a small selector in the panel lets you switch (starting a fresh conversation with the new backend). Fully optional and off by default; skip this section if you don't need either.
Configuration lives in two places that stack: env vars (ANTHROPIC_API_KEY, CG_LOCAL_LLM_BASE_URL/CG_LOCAL_LLM_MODEL/CG_LOCAL_LLM_API_KEY, CG_CLAUDE_MODEL) set a deployment-wide default, and the in-app Settings page (/settings) lets any signed-in user override them for themselves without touching the deployment's environment. A value saved through the Settings page is scoped to the GitHub account that saved it (settings table's PRIMARY KEY (key, user_id)) — never visible to, or overwritable by, a different signed-in account, and never falls back to another account's saved value. If GitHub sign-in (above) isn't configured at all, or a visitor isn't signed in, Settings changes go to a single shared "no account" bucket instead — the same single-shared-config behavior as before this scoping existed, appropriate for a self-hosted single-operator instance.
Runs in-process against the repo's live workspace directory.
- Set
ANTHROPIC_API_KEY=<your key>on the deployment (and/or in a local.env.localfor dev). Restart/redeploy. npm installalready pulls the correct platform binary via@anthropic-ai/claude-agent-sdk'soptionalDependencies(Linux glibc/musl x64+arm64, macOS, Windows), so this works in the Docker image with no extra install step.- To use your Claude Pro/Max/Team subscription instead of an API key (the "Use my Claude subscription" toggle on the Settings page), CodeGraph requires real evidence this exact server process can authenticate — an
ANTHROPIC_API_KEY-free login. On a hosted/container deployment (Render, Docker, etc.) there's no persistent home directory and no interactive terminal to ever runclaude logindirectly on the server, so the only way this works is a portable long-lived token:- On your own computer (not the server) — run
npx @anthropic-ai/claude-code setup-token(orclaude setup-tokenif you already have Claude Code installed globally). This is a real subcommand of the official CLI:Set up a long-lived authentication token (requires Claude subscription). - It opens a browser to sign in with your Claude.ai account (Pro/Max/Team) — or prints a URL/code to paste manually if a browser can't open (e.g. over SSH).
- On success it prints a token valid for 1 year:
Your OAuth token (valid for 1 year): ... Use this token by setting: export CLAUDE_CODE_OAUTH_TOKEN=<token>. Copy it now — you won't be able to see it again. - Set
CLAUDE_CODE_OAUTH_TOKEN=<that token>on the deployment (Render dashboard → your service → Environment → Add Environment Variable) and redeploy/restart. - CodeGraph checks for this evidence server-side (
claudeSubscriptionCredentialsAvailable()inlib/settings.ts) before ever offering Claude via the subscription path, so the toggle only "just works" once the token is actually set — no server-side login needed beyond this one env var.
- On your own computer (not the server) — run
Talks over plain HTTP to a model server running on your own hardware — no data leaves the machine running CodeGraph. Tested against the OpenAI-compatible /v1/chat/completions shape that Ollama, LM Studio, llama.cpp's server, vLLM, and text-generation-webui all implement; tool-calling quality (and therefore how well the assistant can actually edit files) depends entirely on the model you pick — recent tool-calling-tuned models (e.g. Qwen2.5-Coder, Llama 3.1+) work noticeably better than older/small ones.
- Start your model server and note its OpenAI-compatible base URL, e.g.
http://localhost:11434/v1for Ollama,http://localhost:1234/v1for LM Studio. - Set
CG_LOCAL_LLM_BASE_URLandCG_LOCAL_LLM_MODELon the deployment (and/or in a local.env.localfor dev). Restart/redeploy. - If running CodeGraph itself in Docker while the model server runs on the host, point
CG_LOCAL_LLM_BASE_URLat the host (e.g.http://host.docker.internal:11434/v1on Docker Desktop) rather thanlocalhost, which inside the container means the container itself.
An "AI Assistant" icon appears in the Editor tab's activity bar automatically once at least one backend is configured — no other config needed, and nothing changes for anyone if you leave both unset.
What this grants: nothing beyond the app's own workspace tools, for either backend. The assistant gets zero built-in Claude Code tools (tools: [], strictMcpConfig: true, settingSources: [] in lib/agents/assistant.ts) and the local-model path has no built-in tools to begin with — no Bash, no raw filesystem access, no project settings/hooks/plugins, for either. Both backends' only capabilities are the same nine tools (lib/agents/workspaceToolImpls.ts), thin wrappers over the exact path-safe helpers the human-facing Editor already uses (lib/workspace.ts's resolveSafe-guarded fs ops, lib/gitops.ts's argv-only git wrapper) — the same traversal/injection guarantees apply regardless of which model is driving the chat. Sessions are per-repo, in-process, and never written to disk (persistSession: false for Claude; the local backend never had a disk-persistence path to begin with); a server restart drops all AI Assistant conversation history with no cleanup required.
GET /api/health→{ status: "ok", localAccessAllowed }(200). Use for LB/Docker/K8s probes. Deliberately excludesuptime/timestamps — unauthenticated reconnaissance value with no legitimate client use.- Jobs are persisted in SQLite; a crashed indexing job is retriable by re-submitting.
- Indexing runs in-process (fire-and-forget). For high throughput, front with a queue (future work).
- Anonymous git URLs must be public;
GIT_TERMINAL_PROMPT=0prevents credential prompts. A signed-in GitHub user (see above) can additionally clone their own private repos, authenticated with their own OAuth token — never anyone else's. - Repo URL is validated (
^https?://…) and SSRF-guarded (rejects loopback/private/link-local hosts, seelib/urlSafety.ts) before use; clone runs with a timeout + output cap. - Local-folder indexing is off by default on a public deployment (
CG_ALLOW_LOCAL_ACCESS) — self-hosted/trusted-host use only; never turn it on for untrusted visitors on a shared host. - No secrets are required for the core product — the agent swarm is deterministic and needs no LLM API key. GitHub sign-in (needs a secret) and the two optional AI Assistant backends (Claude needs a secret; the local-model backend needs no secret at all — it's just an HTTP call to hardware you already control) are the only opt-in extras; everything else runs with zero credentials.
- Move indexing to a worker queue (Redis/BullMQ) — API stays;
store.createIndexJobenqueues. - Swap SQLite → Postgres (
pgvector) via the samestoreinterface for multi-tenant scale + real embeddings. - Incremental re-index on webhooks (only changed files re-parsed).
npm run test # vitest unit tests (engines)
npm run build # typecheck + standalone build- State is a single file:
data/codegraph.sqlite(+-wal,-shm). Back up thedatavolume. - Restore by placing the file back and restarting.
- Indexing a git repo now clones it into
data/workspaces/<repoId>/(depth 50, all branches) instead of a disposable temp dir, so the Editor tab has a live working tree to read/write/commit against. This lives on the same persistent disk as the SQLite DB — bumpdisk.sizeGBinrender.yaml(or thecodegraph-datavolume) if you index many/large repositories. - Deleting a git-sourced repo removes its
workspaces/<repoId>directory. Deleting a local-sourced repo never touches disk — its "workspace" is the user's real folder passed in at index time. - The remediation executor (
/api/repos/:id/fix) is unaffected: it still clones into a disposableos.tmpdir()sandbox and removes it when done.