Serve a repository
npx @web-git-graph/node serve --repo . \
--port 4174 \
--cors-origin http://127.0.0.1:4173
Binds to 127.0.0.1 by default. CORS stays off unless you enable it.
Protocol reference
Six read-only JSON endpoints. Implement them in any language and <web-git-graph> can render your history — no SDK required.
application/vnd.web-git-graph.v1+json01Quick start
The reference server ships with @web-git-graph/node; the browser client with @web-git-graph/web. Either side can be replaced.
npx @web-git-graph/node serve --repo . \
--port 4174 \
--cors-origin http://127.0.0.1:4173
Binds to 127.0.0.1 by default. CORS stays off unless you enable it.
import { HttpGitGraphProvider } from "@web-git-graph/web/providers/http";
graph.provider = new HttpGitGraphProvider({
baseUrl: "http://127.0.0.1:4174",
repositoryId: "local"
});
The provider sends Accept: application/vnd.web-git-graph.v1+json and handles paging, comparison, and diffs.
02Endpoints
Repository ids are opaque tokens minted by the server — filesystem paths never cross the wire. Cursors are opaque too; treat them as bookmarks, not offsets.
/v1/capabilities
Feature discovery. The component reads this once and hides anything the backend does not support.
{
"protocolVersion": "1",
"history": true,
"details": true,
"compare": true,
"diff": true,
"workingTree": true,
"stashes": true,
"maxPageSize": 200
}
/v1/repositories
Lists the repositories this server exposes. The id is the only handle a client ever holds.
[
{
"id": "local",
"name": "web-git-graph",
"bare": false,
"head": "a91de840c1f2…"
}
]
/v1/repositories/{id}/history
One page of commits plus the refs that point into it. Follow cursor while hasMore is true; lanes stay stable across pages.
| Query | Description |
|---|---|
ref | Branch, tag, or commit to start from (default: HEAD) |
cursor | Opaque cursor from the previous page |
limit | Page size, capped by maxPageSize |
includeWorkingTree | Include the working-tree row (default: true) |
{
"commits": [
{
"oid": "a91de840c1f2…",
"parents": ["3f18d220…", "be901ad0…"],
"message": "Merge release/0.1 into main",
"kind": "commit",
"author": { "name": "Mira Chen" },
"committedAt": "2026-07-27T08:18:00Z"
}
],
"refs": [
{ "name": "refs/heads/main", "target": "a91de840c1f2…", "kind": "head" }
],
"head": "a91de840c1f2…",
"cursor": "op9GdWJHaXRHcmFwaA…",
"hasMore": true,
"repositoryId": "local",
"repositoryName": "web-git-graph"
}
/v1/repositories/{id}/commits/{oid}
Full details for one commit: message body, refs, and the changed files. Use the literal oid __WORKTREE__ for the working tree.
{
"commit": {
"oid": "a91de840c1f2…",
"message": "Merge release/0.1 into main",
"kind": "commit"
},
"refs": [
{ "name": "refs/tags/v0.1.0-rc.1", "target": "a91de840c1f2…", "kind": "tag" }
],
"changes": [
{
"path": "packages/web/src/layout.ts",
"kind": "modify",
"additions": 18,
"deletions": 4
}
],
"body": "Full commit message body."
}
/v1/repositories/{id}/compare
The tree difference between two revisions. A revision is {"kind":"commit","oid":…}, {"kind":"stash","oid":…}, or {"kind":"working-tree"}.
{
"base": { "kind": "commit", "oid": "86d41b10…" },
"head": { "kind": "commit", "oid": "a91de840…" }
}
{
"base": { "kind": "commit", "oid": "86d41b10…" },
"head": { "kind": "commit", "oid": "a91de840…" },
"changes": [
{
"path": "packages/node/src/backend.ts",
"kind": "modify",
"additions": 42,
"deletions": 7
}
],
"additions": 42,
"deletions": 7
}
/v1/repositories/{id}/diff
A unified patch for a single file, loaded lazily when a row is expanded. Oversized output is truncated instead of failing.
{
"base": { "kind": "commit", "oid": "86d41b10…" },
"head": { "kind": "working-tree" },
"path": "packages/web/src/layout.ts",
"context": 3
}
{
"base": { "kind": "commit", "oid": "86d41b10…" },
"head": { "kind": "working-tree" },
"path": "packages/web/src/layout.ts",
"patch": "@@ -12,7 +12,9 @@ export function computeLanes(…"
}
03Errors
Every non-2xx response carries the same body. retryable tells the client whether trying again can help; a 409 snapshot_expired means the cursor is stale — restart from the first page.
{
"error": {
"code": "revision_not_found",
"message": "Commit 1234abcd was not found.",
"retryable": false
}
}
| code | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Malformed route, query, or body |
unauthorized | 401 | The authorize hook rejected the request |
forbidden | 403 | Authenticated but not allowed |
repository_not_found | 404 | Unknown repository id |
revision_not_found | 404 | Unknown commit, stash, or ref |
snapshot_expired | 409 | Cursor no longer valid — reload from page one |
output_limit | 413 | Request or response exceeded a size limit |
unsupported | 422 | The backend does not implement this capability |
rate_limited | 429 | Too many requests — retry later |
internal_error | 500 | Unexpected server failure |
git_unavailable | 503 | Git could not be executed on the server |
04Build a backend
The contract is just HTTP and JSON. Implement the six routes in Go, Rust, Python, Java — whatever already runs in your stack.
The protocol package ships the DTOs, JSON Schemas, and the OpenAPI document — use them to validate your implementation.
import {
GIT_GRAPH_JSON_SCHEMAS
} from "@web-git-graph/protocol";
import {
OPENAPI_DOCUMENT
} from "@web-git-graph/protocol/http";
A fetch-style handler with an authorize hook drops into Node, and the local backend is hardened: opaque ids, no shell, size limits.
import {
LocalGitBackend,
createGitGraphFetchHandler
} from "@web-git-graph/node";
const handle = createGitGraphFetchHandler({
backend: new LocalGitBackend({ repository: "." }),
authorize: ({ request }) => checkToken(request)
});