Protocol reference

The HTTP v1 protocol.

Six read-only JSON endpoints. Implement them in any language and <web-git-graph> can render your history — no SDK required.

Media type
application/vnd.web-git-graph.v1+json
Access
Read-only — no Git mutation exists in the contract
Versioning
All routes live under /v1; the media type carries the version

01Quick start

Two sides of the wire.

The reference server ships with @web-git-graph/node; the browser client with @web-git-graph/web. Either side can be replaced.

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.

Connect the component

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

Six routes, one contract.

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.

GET /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
}
GET /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…"
  }
]
GET /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.

QueryDescription
refBranch, tag, or commit to start from (default: HEAD)
cursorOpaque cursor from the previous page
limitPage size, capped by maxPageSize
includeWorkingTreeInclude 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"
}
GET /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."
}
POST /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
}
POST /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

One error shape.

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
  }
}
codeHTTPMeaning
bad_request400Malformed route, query, or body
unauthorized401The authorize hook rejected the request
forbidden403Authenticated but not allowed
repository_not_found404Unknown repository id
revision_not_found404Unknown commit, stash, or ref
snapshot_expired409Cursor no longer valid — reload from page one
output_limit413Request or response exceeded a size limit
unsupported422The backend does not implement this capability
rate_limited429Too many requests — retry later
internal_error500Unexpected server failure
git_unavailable503Git could not be executed on the server

04Build a backend

Any language works.

The contract is just HTTP and JSON. Implement the six routes in Go, Rust, Python, Java — whatever already runs in your stack.

Typed contract, exported

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";

Or reuse the Node handler

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)
});

Six endpoints between Git and your UI.