Self-hosted code search and relationship-aware code intelligence for humans and AI agents.
GraphNest is an experimental, self-hosted code search and context layer for engineering teams and AI agents. Authorized clients can search GitHub repositories, open files at the exact indexed commit, follow symbols across repositories, and inspect dependency and impact relationships without receiving direct access to the underlying search index.
Under the hood, GraphNest combines fast Zoekt search, SCIP code navigation, and a PostgreSQL relationship graph behind web, REST, and MCP interfaces.
Important
GraphNest is pre-1.0 pilot software. It is not production-ready, currently indexes default branches only, and has not yet been validated at production scale or certified against a live GitHub Enterprise Server or OpenShift environment. See Compatibility for the current boundaries.
| License dashboard | Dependency graph |
|---|---|
![]() |
![]() |
| Capability | Description |
|---|---|
| Fast, scoped code search | Search authorized repositories through a server-controlled Zoekt backend. Clients never receive direct Zoekt access or choose raw Zoekt repository IDs. |
| Exact indexed revisions | Open files at the precise indexed commit. Search results are suppressed when Zoekt and PostgreSQL disagree about the current indexed SHA. |
| Cross-repository code navigation | Upload pre-generated SCIP indexes to navigate definitions, references, and implementations without running language indexers inside GraphNest. |
| Relationship-aware graph analysis | Explore bounded context, impact, and dependency paths directly from PostgreSQL. |
| Dependencies & Licenses inventory (opt-in) | Collect GitHub dependency-graph SBOMs on a schedule, preserve every original document byte-for-byte as an immutable snapshot, and browse or download the authorized repository inventory without depending on Zoekt, SCIP, or an indexed commit. |
| Human and agent interfaces | Use the embedded browser console, REST API, hosted Streamable HTTP MCP endpoint, or the graphnest-mcp stdio proxy. |
| GitHub-native repository management | Reconcile GitHub App installations, verify webhook signatures, queue default-branch indexing, support private CAs, and retain numeric GitHub repository identity across renames. |
| Durable identity and access | Use OIDC or GitHub OAuth browser sign-in, SCIM 2.0 provisioning, revocable API tokens, user and group repository assignments, administrative controls, and security audit events. |
| Pilot deployment tooling | Run locally with Docker Compose or deploy the single-node pilot with Helm. Releases publish multi-architecture images, an OCI chart, SBOMs, provenance, and GitHub attestations. |
An optional native enrichment binary supports Go, JavaScript, TypeScript/TSX, Java, Kotlin, and Rust. When configured, the indexer invokes it on the same archive snapshot; it is not a standalone worker and is not part of the default images or deployment. SCIP uploads remain a separate, language-indexer-independent navigation path.
flowchart LR
Client[Web UI / REST / MCP] --> Server[graphnest-server]
Server -->|GitHub App API| GitHub[GitHub.com or GHES]
GitHub -->|Signed webhooks| Server
Indexer[graphnest-indexer] -->|Download default-branch archive| GitHub
Server --> Postgres
Indexer --> Postgres
Postgres --> Indexer
Indexer --> Zoekt[(Zoekt index)]
Server --> Zoekt
Server --> Postgres
PostgreSQL is authoritative for repository metadata, authorization, queues, indexed-SHA state, graph artifacts, and graph queries. Zoekt is a private query store reached only through GraphNest's authenticated services. See Architecture and the accepted decisions under docs/adr.
GRAPHNEST_SEARCH_BACKEND defaults to zoekt. Durable deployments may set it to
github for low-volume degraded code search: every request remains
authorization-scoped, but results are best-effort, may be partial, and never
claim an exact indexed SHA. It does not automatically fall back between backends.
The architecture decision index records accepted and superseded design decisions.
| Interface | Location | Authentication |
|---|---|---|
| Browser console | /, /repositories, /supply-chain (Licenses and Dependency graph views), /admin, /account |
Development bearer token or durable OIDC or GitHub OAuth session |
| REST API | /v1/... |
Bearer token or, where supported, same-origin browser session |
| Streamable HTTP MCP | /mcp |
Bearer API token, or an OAuth access token obtained through the built-in authorization server |
| Stdio MCP proxy | graphnest-mcp |
Uses GRAPHNEST_SERVER_URL and GRAPHNEST_TOKEN |
| Command-line tool | graphnest (login, logout, graph import codegraph, graph status) |
Uses GRAPHNEST_SERVER_URL and GRAPHNEST_TOKEN, GRAPHNEST_TOKEN_FILE or a browser sign-in with graphnest login; offline for a dry run |
| Health and observability | /healthz, /readyz, /metrics |
Intended for deployment health checks and monitoring |
REST routes accept exactly one bearer credential or browser session; mixed credentials are rejected. MCP remains bearer-only; with GRAPHNEST_MCP_OAUTH=true MCP clients obtain that bearer token themselves through OAuth 2.1 (see Operations).
The complete REST contract is available in docs/openapi.yaml.
The fixture profile is the fastest way to try GraphNest. It starts a deterministic test repository and Zoekt index while the server runs on the host.
- Go 1.27.1
- Node 26.10.0 (builds the embedded web console)
- Git
- Docker with Docker Compose
jq- Internet access for Go tools and container images
git clone https://github.com/balcsida/graphnest.git
cd graphnest
make tools
docker compose -f deploy/compose/compose.yml --profile fixture up -d --waitThe fixture is indexed as repository fixture/repository with Zoekt repository ID 7.
In another terminal:
GRAPHNEST_LISTEN_ADDRESS=127.0.0.1:8080 \
GRAPHNEST_ZOEKT_URL=http://127.0.0.1:6070 \
GRAPHNEST_REPOSITORIES_FILE=deploy/compose/repositories.json \
GRAPHNEST_USER_TOKEN=graphnest-dev-user-token \
GRAPHNEST_ADMIN_TOKEN=graphnest-dev-admin-token \
GRAPHNEST_USER_REPOSITORIES=fixture/repository \
GRAPHNEST_ADMIN_REPOSITORIES=fixture/repository \
go run ./cmd/graphnest-serverOpen http://127.0.0.1:8080/ and sign in with:
graphnest-dev-user-token
The browser keeps this development token only for the current session.
The browser console is a React single-page application under web/, embedded into graphnest-server from internal/webui/dist. Building it needs Node 26.10.0:
make ui # npm ci and a production build into internal/webui/dist
make ui-dev # Vite dev server, proxying the API to 127.0.0.1:8080
make ui-check # ESLint, type check, and Vitest
make ui-screenshots # refresh docs/images/console-*.png with Playwrightmake build, make test, make test-race, make server, and the container image builds run make ui automatically. A Go binary built without the console (for example a plain go build) still starts, but answers the console routes with 503 Service Unavailable. See ADR-0018.
curl --fail-with-body http://127.0.0.1:8080/v1/search \
-H 'Authorization: Bearer graphnest-dev-user-token' \
-H 'Content-Type: application/json' \
--data '{
"query": "GraphNestFixtureNeedle",
"repositories": ["fixture/repository"]
}'Requests for repositories outside the authenticated principal's scope return no matches rather than revealing whether those repositories exist.
Queries use Zoekt query syntax. Use case:no for a case-insensitive search. Inline flags such as (?i) are refused, because Zoekt ignores them and returns wrong matches. Spaces separate query terms, so quote a term that contains spaces ("FROM node:[0-9 ]"). An invalid query returns 400 with a message naming the offending construct, for example search query is invalid: error parsing regexp: missing closing ]: ..., followed by a hint when GraphNest recognises the mistake.
docker compose -f deploy/compose/compose.yml --profile fixture downThe web console's Connect an agent page (/connect) shows copy-ready setup for common agent clients, filled in with this server's address.
MCP clients that support Streamable HTTP can connect directly to:
http://127.0.0.1:8080/mcp
Send the same bearer token in the Authorization header. The core tools include code search and file discovery; durable mode additionally exposes symbol navigation and graph-backed analysis, which answer for a repository once a SCIP index has been uploaded for its indexed commit (see SCIP code navigation).
For a stdio-only MCP client, build the proxy:
go build -o /tmp/graphnest-mcp ./cmd/graphnest-mcp
GRAPHNEST_SERVER_URL=http://127.0.0.1:8080 \
GRAPHNEST_TOKEN=graphnest-dev-user-token \
/tmp/graphnest-mcpThe proxy appends /mcp automatically and does not connect to Zoekt directly.
GraphNest also ships optional graph-analysis skills for agent clients. Installation is explicit and does not happen during normal proxy startup:
/tmp/graphnest-mcp install-skills --root /path/to/repositoryThe installer writes GraphNest-owned content under .claude/skills/ and mirrors it to .agents/skills/ only when .agents/ already exists.
GraphNest stores SCIP indexes but does not generate them. Produce the .scip file in each repository's CI for the same 40-character lowercase commit SHA reported by GraphNest as indexed_sha, then upload it with an administrator token:
scip-go
curl --fail-with-body -X POST \
"https://graphnest.example/v1/scip/uploads?repository_id=101&commit=$GITHUB_SHA" \
-H "Authorization: Bearer $GRAPHNEST_ADMIN_TOKEN" \
-H 'Content-Type: application/vnd.scip+protobuf' \
--data-binary @index.scipThe same upload derives the graph generation behind graph_callers, graph_discover, explore and the other graph tools; get_repository_status reports it as graph_status, and a graph tool called before the upload returns graph_missing (see Operations). Uploads for any commit other than the repository's exact indexed SHA are rejected. An upload for the commit GraphNest is still indexing returns 409 with code index_pending and retryable: true; CI usually finishes before the asynchronous index job, so retry until indexing completes:
for _ in $(seq 40); do # every 30 seconds for up to 20 minutes
code=$(curl -s -o response.json -w '%{http_code}' -X POST \
"https://graphnest.example/v1/scip/uploads?repository_id=101&commit=$GITHUB_SHA" \
-H "Authorization: Bearer $GRAPHNEST_ADMIN_TOKEN" \
-H 'Content-Type: application/vnd.scip+protobuf' \
--data-binary @index.scip)
[ "$code" = 409 ] && grep -q '"index_pending"' response.json || break
sleep 30
done
[ "$code" = 204 ] || { cat response.json; exit 1; }The upload is ingested synchronously and returns 204 only after the index is committed, so a large index can take minutes; any reverse proxy or ingress in front of GraphNest needs a matching response timeout (see the Helm chart documentation). Cross-repository navigation can use manually supplied package URLs or metadata refreshed from GitHub's dependency graph. The exact endpoints, limits, and response schemas are defined in the OpenAPI contract.
A CI job should not hold a long-lived administrator token. Instead, a trusted broker that owns an administrator API token can delegate a narrower one per job with POST /v1/admin/api-tokens: the delegated token belongs to the same user, is restricted to a non-empty subset of the broker token's repository ceiling (typically the one repository being indexed), and must expire within one hour. Only administrator API tokens may delegate, and a delegated token cannot delegate again, so a leaked job token cannot renew itself past its own expiry; browser sessions keep using /v1/account/api-tokens.
curl --fail-with-body -X POST "https://graphnest.example/v1/admin/api-tokens" \
-H "Authorization: Bearer $GRAPHNEST_BROKER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"repository_ids":[101],"expires_at":"2026-08-01T00:15:00Z"}'The graphnest command reads an existing CodeGraph index (.codegraph/codegraph.db, schema versions 9, 10 and 11 from CodeGraph 1.6.0 to 1.6.2) and converts it to the graph artifact that POST /v1/graph/uploads accepts, without re-indexing, without CodeGraph installed, and without cgo. The reader opens the database read-only inside one transaction, so a CodeGraph watcher or daemon that still has it open is left alone and nothing under .codegraph/ is written.
go build ./cmd/graphnest
./graphnest graph import codegraph --dry-run --repo . --repository-id 101
./graphnest graph import codegraph --output graph.pb --repo . --repository-id 101The dry run prints one JSON document on stdout: the index path, schema and producer version, the commit (git rev-parse HEAD in --repo, or --commit), counts by node and edge kind, unresolved-reference and extraction-error diagnostics, the artifact size and content hash, a freshness block, and published: false. Diagnostics go to stderr; the exit code is 0, 1 for a failed import and 2 for a usage error.
Freshness is checked against the commit's own content, not the working tree: graphnest streams git archive of the commit and hashes every file the index covers the way CodeGraph does (SHA-256 of Node's UTF-8 decoding, or CodeGraph 1.6.1's size stamp above 1 MiB), then lists modified files, indexed files that are not_in_commit (untracked or deleted content), and source files at the commit that are not_indexed according to CodeGraph's own extension and ignore rules, the commit's root .gitignore and its codegraph.json. A matching HEAD or a clean checkout is not proof; only status: fresh is. Indexes from an unknown CodeGraph version, or whose files are symlinks or submodules at the commit, are unverifiable. --output FILE writes the artifact only when the index is fresh and CodeGraph reports it complete; otherwise it prints the same report, says why on stderr and exits 1. Without --dry-run or --output, the same command publishes a fresh, complete index: it reads the server's indexed commit and the publication preflight, refuses when the commit differs from the index's, when the token lacks publication rights, when --expected-generation does not name the active published generation, or when another producer's generation is active and --replace-producer is absent, then uploads with the observed generation as its precondition. graphnest graph upload graph.pb --repository-id 101 publishes an artifact written earlier with the same checks. Transport and server failures are retried up to three times; the server deduplicates identical content, so a retry after a lost response is reported as deduplicated rather than published twice. A failed upload leaves the previous generation active, and SCIP navigation is never affected. graphnest graph status --repository-id 101 shows the repository and graph state the server holds, using the same environment variables as graphnest-mcp (GRAPHNEST_SERVER_URL, and GRAPHNEST_TOKEN or a file named by GRAPHNEST_TOKEN_FILE; GRAPHNEST_CA_FILE adds a private CA). Without either token variable, every server command uses the login stored by graphnest login, a browser sign-in to GRAPHNEST_SERVER_URL; the token variables take precedence, and graphnest logout revokes and forgets the login. The token is never accepted on the command line and never follows a redirect to another host. See Operations.
On macOS or Linux with Homebrew, brew install balcsida/tap/graphnest installs the release binary; the tap updates its formula after each release.
Each GitHub release attaches graphnest binaries for linux/amd64, linux/arm64, darwin/amd64 and darwin/arm64 (graphnest_<version>_<os>_<arch>), graphnest_<version>_checksums.txt and graphnest_<version>_dependencies.txt, the module inventory of the Linux build. They are static, need no CodeGraph and no cgo, and are attested with build provenance. Download a binary and the checksums file, then verify and run it:
sha256sum -c --ignore-missing graphnest_<version>_checksums.txt # shasum -a 256 -c on macOS
gh attestation verify graphnest_<version>_<os>_<arch> --repo balcsida/graphnest
chmod +x graphnest_<version>_<os>_<arch>
./graphnest_<version>_<os>_<arch> doctor --repo /path/to/checkoutgraphnest doctor is the first command to run: it checks, read-only, that git works, that --repo has a commit, that the CodeGraph index is readable and fresh, and, when GRAPHNEST_SERVER_URL is set, that the server accepts the credentials. graph import codegraph and graph upload take --format text for a readable report instead of JSON. make cli VERSION=<version> builds the same files into dist/cli/.
With GRAPHNEST_SUPPLY_CHAIN=true in durable mode, graphnest-server collects each managed repository's dependency-graph SBOM export from GitHub on a jittered schedule (GRAPHNEST_SUPPLY_CHAIN_INTERVAL, default 24h), preserves the original SPDX 2.3 JSON document and its SHA-256, normalizes component occurrences and relationships into an immutable snapshot, and serves them under /v1/supply-chain/... and the embedded console at /supply-chain, which includes a License dashboard (/supply-chain/licenses) and a Dependency graph of repositories and their shared external dependencies (/supply-chain/graph). The module is disabled by default; enabling it centrally requires no change to any repository.
What the inventory is and is not:
- A GitHub dependency-graph export is a timestamped observation of the default branch. The endpoint has no ref selector, so snapshots report
subject_assurance: unknown; GraphNest never copies the indexed or current HEAD into a snapshot. - GitHub Enterprise Server does not populate dependency license fields;
license_declared_raw/license_concluded_raware preserved verbatim (typicallyNOASSERTION) and are never mapped to a license. Exact-version license evidence comes only from registry routes you configure (GRAPHNEST_SUPPLY_CHAIN_REGISTRY_{NPM,NUGET,MAVEN}_URLand companion secret-file settings); without a route no license traffic is produced, and a private route never falls back to a public registry. SPDX expressions are parsed against the pinned SPDX License List 3.27.0 with AND/OR/WITH structure preserved;NOASSERTION,NONE,UNLICENSED, unknown identifiers, license files, and URLs stay what they are. - Components without a purl or version stay visible. Dependency scope (
root/direct/transitive) is derived only from resolvedDEPENDS_ONedges leaving a described root; a flattened list yieldsunknown, neverdirect. - A failed refresh (403, 404, rate limit, malformed or oversized document, outage) records a collection attempt and leaves the last successful snapshot in place; the status reports
collection: failedalongside the retained inventory. - Inventory eligibility is repository authorization alone. It works for repositories with no Zoekt index, no SCIP upload, and no graph enrichment, and inventory work never blocks lexical indexing.
License review is a separate, auditable layer: reviewers with a repository-scoped grant record human conclusions and approve/reject/exception decisions against the exact evidence they saw (a changed evidence fingerprint is refused), versioned policies are evaluated over the SPDX expression tree (the shipped policy is a labelled example, and unknown licensing never auto-approves), and three read-only MCP tools expose the inventory to agents through the same authorization as REST.
SBOMs produced elsewhere (Syft, ORT, or any tool writing SPDX 2.3 JSON or CycloneDX 1.6 JSON) can be imported into separate import:<subject>:<label> streams with POST /v1/supply-chain/imports; the uploader is recorded apart from the producer the document claims, and a derived SPDX export links back to the preserved original. Portfolio views (/v1/supply-chain/overview, /components, /facets, exports, comparison) aggregate only over the caller's authorized repositories and name every denominator.
Every read resolves the live principal's repository scope before any inventory row is touched; snapshot and job identifiers outside that scope are indistinguishable from missing ones. Manual refresh (POST /v1/supply-chain/repositories/{id}/refresh) only enqueues a bounded background job and requires administrator access. The published snapshot is also projected into the existing GitHub-sourced SCIP package mappings; manual mappings are never touched. See Operations and ADR-0017.
Static fixture mode is intentionally small. Durable mode adds PostgreSQL-backed repository state, GitHub App reconciliation, verified webhook ingestion, queued indexing, exact-SHA file reads, identity management, and graph analysis.
A durable deployment consists of:
graphnest-serverfor the web UI, REST, MCP, authentication, authorization, and GitHub reconciliation;- one
graphnest-indexerfor leased default-branch indexing and Zoekt publication; - PostgreSQL as the authoritative state store; and
- Zoekt as private query infrastructure.
The Compose deployment requires application and node images, PostgreSQL, GitHub App credentials, and the server settings documented in Operations:
docker compose \
-f deploy/compose/compose.yml \
-f deploy/compose/durable.yml \
--profile durable \
up -d --wait| Purpose | Mechanism |
|---|---|
| Local fixture access | Distinct development-only user and administrator bearer tokens |
| Browser sign-in | OIDC or GitHub OAuth Authorization Code flow with PKCE and an opaque, HttpOnly GraphNest session |
| REST and MCP access | Revocable bearer API tokens; /mcp remains bearer-only |
| MCP client sign-in | Optional OAuth 2.1 authorization server (GRAPHNEST_MCP_OAUTH): dynamic client registration, PKCE, consent page, hour-long access tokens with rotating refresh |
| Directory provisioning | Optional SCIM 2.0 endpoint protected by a dedicated secret-file token |
| Emergency administration | Disabled-by-default local recovery flow provisioned offline with graphnest-admin |
Authorization is enforced by the server against current repository IDs and directory state. Repository names are selectors, not security identities. Deactivated users and revoked credentials are denied on their next request.
GitHub OAuth uses a dedicated OAuth App per environment, separate from the GitHub App used for repository access. Configure GRAPHNEST_PUBLIC_URL, GRAPHNEST_OAUTH_GITHUB_CLIENT_ID, and GRAPHNEST_OAUTH_GITHUB_CLIENT_SECRET_FILE, then register https://<public-host>/auth/oauth/github/callback. The flow requests no scope and uses the access token once for GET /user; the token is then discarded and cannot authenticate MCP. GitHub Enterprise Server OAuth remains unverified.
Optionally, GraphNest can inherit repository access from GitHub instead of SCIM. With GRAPHNEST_OAUTH_GITHUB_ACCESS_SYNC=true, the OAuth client must be the GitHub App's own OAuth credential; each sign-in then provisions the user on first use and replaces their GitHub-derived grants with the indexed repositories that user can access through the App's installations. Administrator remains an explicit GraphNest role. See Operations.
OIDC, GitHub OAuth, SCIM, API-token administration, audit events, and break-glass recovery require durable mode. See Operations and the Threat model before exposing the service.
The chart under deploy/helm/graphnest targets Kubernetes 1.25 or newer and models a generic single-node pilot. It expects operator-managed PostgreSQL and existing Kubernetes Secrets; it does not install a database or place plaintext credentials in chart values.
Released OCI charts embed immutable application and node image digests. For the current chart version:
Deployments created before the GraphNest rename require fresh GraphNest configuration and a fresh GraphNest installation. This is not an in-place upgrade; GraphNest does not automatically discover or mutate previous deployment state. The helm upgrade --install example below applies only to upgrades between GraphNest releases.
helm pull oci://ghcr.io/balcsida/graphnest/charts/graphnest --version 0.2.0
helm upgrade --install graphnest graphnest-0.2.0.tgz \
--namespace graphnest \
--create-namespace \
--values my-values.yaml \
--wait \
--timeout 15mReview the Helm chart documentation for required images, Secrets, storage, ingress, network policies, OIDC, GitHub OAuth, SCIM, monitoring, and recovery procedures. Follow the archive and PostgreSQL graph migration when upgrading an older deployment. Release notes contain immutable artifact references and attestation-verification commands.
- GraphNest is pre-1.0 pilot software and makes no stable compatibility promise.
- Only default branches are indexed.
- The default GHES contract targets GitHub Enterprise Server 3.17 with REST API version
2022-11-28; this has not been certified against a live GHES deployment. - Kubernetes, OpenShift, backup and restore, upgrade and rollback, ingress, and production-scale capacity still require environment-specific validation.
- Optional native graph scanning is not equivalent to a full language server or language-specific indexer.
- GraphNest does not currently provide embedding-based semantic search.
- PostgreSQL graph data follows the same backup and recovery policy as other durable repository state.
Read Compatibility, Benchmarking, and Operations before planning a pilot.
Build the commands and local images with:
make build
make image-testRun the main verification suites with:
make fmt lint staticcheck govulncheck
make test test-race integration e2e
make openapi-check compose-test helm-lint helm-testCI additionally exercises scanner grammar compatibility, UI smoke tests, and release packaging. Some targets download pinned tools and require Docker.
| Document | Purpose |
|---|---|
| Architecture | Service boundaries, authorization flow, indexing, and graph queries |
| Operations | Local and durable operation, recovery, identity, and graph runbooks |
| OpenAPI | Canonical REST request, response, security, and limit contract |
| Helm chart | Kubernetes configuration, Secrets, storage, networking, and installation |
| Compatibility | Supported contracts, platforms, languages, and unverified boundaries |
| Threat model | Protected assets, security controls, and known limits |
| Benchmarking | Measurement guidance for pilot sizing |
| Release process | Signed tags, images, OCI chart, attestations, and release verification |
| Implementation report | Delivered milestones, verification evidence, risks, and deferred work |
| Dependency pinning | Reproducibility and pinned dependency policy |
| Archive and graph migration | Upgrade, verification, cleanup, and rollback procedure |
See the architecture decision index for the complete decision history.
Read CONTRIBUTING.md before proposing a change. Keep changes within an accepted milestone, include tests for behavior changes, and preserve the boundary that Zoekt is private implementation infrastructure.
Use GitHub Issues for reproducible bugs and feature discussions, and read SUPPORT.md for the project's support boundaries. Participation is governed by the Code of Conduct. Report suspected vulnerabilities through the private process in SECURITY.md, not through a public issue.
GraphNest is licensed under the Apache License 2.0.


