One file to understand the whole product. Give this to an engineer, designer, investor, or new teammate and they'll get it.
| Meta | Details |
|---|---|
| Product | RepoGraph — Progressive Context Layer for GitHub |
| Version | 4.1 Consolidated (Human Edition) |
| Status | Active — Surgical MVP built, Gate 0 passed |
| Last Updated | September 2, 2026 |
| Sources Consolidated | Documents/Obsidian Vault/02-Projects/RepoGraph Side Project.md (Validated PRD v2.0) + repograph/PRD.md v4.0 + ARCHITECTURE.md + README.md + RepoGraph Progress.md + RepoGraph — PRD Reconciliation & Validation.md |
| Maintainer | RepoGraph Team |
| License | Core: AGPLv3 / FSL — Enterprise (/ee): Commercial — See §16 |
This isn't a dump of old PRDs stitched together. We took three conflicting PRD versions and the actual code in repograph/ and reconciled them into one story. If you only read one section, read §1 and §4. If you want to build, jump to §8 and §11.
- The Idea in One Sentence
- The Real Problem We're Solving
- Who This Is Actually For
- What RepoGraph Is (and What It Is NOT)
- Product Principles — The Rules We Don't Break
- Where We Fit — Not Another Diagram Tool
- How It Feels to Use — The Journey
- The MVP — What We're Actually Shipping First
- What You Can Do — Feature Spec
- Design Guardrails — How We Avoid a Hairball
- How It Works — Architecture & Tech Choices
- The Data — What We Store
- Getting Data Without Getting Blocked — Ingestion & Rate Limits
- What's After the MVP — AI, Private Repos, Code Intelligence
- How We Make Money (or Don't, Yet)
- Open Source, Not Open Brand — Licensing
- How We Know If It Works — Validation & Metrics
- What Could Kill Us — Risks
- The Plan — Phases, Not Wishes
- Glossary
- Appendix — API Cheat Sheet
Paste any public GitHub repo URL and in under 15 seconds, see how its Issues, PRs, and people actually connect — so you can answer "who owns this, who reviewed it, what should I look at next" without opening eight tabs.
Our north star is simple, and we repeat it whenever we argue about a feature:
RepoGraph should not make developers understand a graph. RepoGraph should use a graph to help developers understand a repository.
DeepWiki tells you what the code does. RepoGraph tells you how the work flowed — who worked with whom, what blocked what, and where you should pay attention.
Anyone who's landed in an unfamiliar repo knows the feeling. You open an issue, you see "Fixes #123", you click, you see a PR, you see a review that mentions someone, you open their profile, you go back... five tabs later you forgot where you started.
We verified the pain is real, but it's not "every developer every day." It's acute for specific people:
- Onboarding is expensive. 44% of orgs say it takes >2 months to ramp a new developer. Leaders report 5+ hours per developer per week just finding context. That’s $20k–$80k per hire wasted on tab-hopping.
- GitHub gives you the pieces, not the connections. You can see an Issue, you can see a PR, you can see a User. But GitHub doesn't show you the relationships in one place.
- After CodeSee shut down (acquired by GitKraken May 2024, gone Feb 2024), there’s no simple, work-focused graph left. Sourcegraph is powerful but $16k/year and about code search, not work flow. GitLens is huge (52M installs) but lives in your editor and shows Git blame, not GitHub Issue → PR → People.
The original PRD tried to fix this for everyone. Validation kicked us in the teeth: score 6.1/10, verdict "Conditional Go — feasible with major surgery." So we narrowed.
We stopped saying "developers exploring unfamiliar repos." That's a demographic, not a job. We picked a beachhead.
Tier 1 — Our First Users (where we prove we matter):
- The OSS Triager / Drive-by Contributor. They evaluate 5–10 unfamiliar public repos a week. They need to find an owner or reviewer fast or they leave. They live on Hacker News, they star things, they tell friends.
- The First-Time Contributor. They just want to go from "First Issue" to "First PR." They ask: "Who reviews my area? What kind of PR actually gets merged?"
Tier 2 — Where the Money Is (after we prove Tier 1 loves us):
- The New Hire / Contractor onboarding to a 100k+ line repo. The buyer is an EM or CTO, the pain is $75k in ramp time.
- The Auditor / Security Reviewer who needs a holistic view every day. Small niche, high willingness to pay.
If Tier 1 doesn't retain, Tier 2 is a fantasy. So the whole MVP is built for Tier 1.
Let's clear this up because it's where most demos die.
RepoGraph IS: A progressive context layer over GitHub. A focused, readable subgraph (never the whole repo) that answers three questions within 1–2 hops: Who is involved? What is connected? What should I look at next?
RepoGraph is NOT:
- A crawler that tries to index a whole repo upfront (we tried that math:
kubernetes/kubernetes= 80k API calls = 16 hours on one token. It never finishes). - A code visualizer or file-tree diagram (use GitDiagram for that).
- A replacement for GitHub. Every node has a big
[Open on GitHub]button. We're a companion.
The MVP Promise we can actually keep:
Any public repo can be focused in <15s to a readable subgraph (<15 nodes to start, <50 total) that answers who owns what and which PRs relate. We never try to build the full repo graph.
If someone expects a hairball with 500 modules, we failed. If they find a reviewer in 30 seconds, we won.
These came from the validated PRD §7 and survived every argument:
- Context over quantity. Hard cap: 400 nodes, 800 edges visible. Focus is DEFAULT ON. If you don't pick a focus, you don't get a graph (API returns 400).
- Progressive disclosure. Start with 10–15 hot items, then let people expand one hop at a time (+ button). Don't dump history.
- Trustworthy edges, or don't show them.
authored,assigned,reviewedare facts.closesis not a fact — GitHub doesn't give us a true close link. We call itcloses_via_keywordand show "Based on 'Fixes #123' — [View on GitHub]" with a confidence tag. Never lie about a connection. - GitHub is the source of truth. We store a derived graph, not raw payloads forever. We revalidate with
ETag/If-None-Match. - Fast subgraph, not fast whole repo.
focusrequired,depth ≤ 2max,timeWindowdefaults to 30 days. Loading 8 years of history is the #1 hairball cause. - No account needed for the first graph. First graph free, no signup. Then we soft-gate (3 graphs/day/IP, then magic-link email). This cuts abuse 80% and builds our list.
- Simple UI. If it needs a tutorial, it’s too complex.
| Category | Who | Their Job | Why We Don't Fight Them |
|---|---|---|---|
| AI Wiki | DeepWiki (Devin) | "What does this code do?" — chat over code | We do "Who worked on what with whom?" — the social/temporal layer they ignore. We're a companion: DeepWiki tells you what code does. RepoGraph tells you how work flowed. |
| Enterprise Search | Sourcegraph | Cross-repo code search | $16k/yr, no social graph. |
| IDE Graph | GitLens | Git history in the editor | Shows git blame, not GitHub Issue → PR → People. |
| One-click diagrams | GitDiagram, Gource | Pretty file trees | No relationship layer, low retention. CodeSee proved pretty graphs alone don't retain. |
Lesson from CodeSee: Don't build a prettier graph. Build a faster answer.
Name collision: ozyyshr/RepoGraph (296★) is a research graph for AI agents. SEO confusion risk. We own repograph, repograph.dev, repograph.app.
The old (broken) journey we killed: Land → paste URL → huge hairball (30 issues + 20 PRs + 40 users) → pan/zoom confusion → can't find Issue #123 → hunt for Focus toggle (80% never find it) → expand to 100 nodes → it's just GitHub again but messier → leave.
The new journey we shipped:
Landing: [github.com/owner/repo ] [Explore] + 1-click demos (harbor / next.js / vscode)
↓ POST /repositories/resolve (parses .git, trailing /)
Skeleton: "Analyzing Harbor... ✓ metadata ✓ 12 issues (30d) ⏳ relationships • You can explore while indexing"
↓
Overview (NO GRAPH YET): Health chips + 3 Lenses:
[Most Discussed 30d] [PRs Awaiting Review] [Recent Activity] + Search bar
↓ User clicks a Lens OR searches "auth"
Focused Graph (<20 nodes, Focus ON, 30d default) + List rail (left) + Details (right)
Lanes left-to-right: Users | Issues | PRs — thickness = signal
Filters: Time 7d/30d/90d/All • Status • Node type • Depth [1 hop][2 hops] (default 1)
Edge hover: "Authored: Sarah created Issue #123 Aug 20 [View]" — one sentence + sourceUrl
↓
Click node → graph re-centers (Esc to go back, URL updates to ?focus=issue:123&depth=1)
Details panel: labels, milestone, people, PRs + [Open on GitHub] + ↻ Sync
↓
+ Expand per node → "8 more hidden" → fetches 1-hop neighbors (lazy) with smooth spring animation
Shareable: ?focus=issue:123&depth=1&timeWindow=30d → copy badge repograph.svg for README
Errors: degraded banner — "Review info rate-limited. Issues/PRs available." never "Something went wrong"
Why this works: We validated via Wizard-of-Oz (n=20, within-subjects crossover) that a hybrid List+Graph with Focus ON beats GitHub alone: +21% absolute task success (p=0.008), -40.8% time-to-correct (p=0.003), SUS 62→76 (+14). Pure graph would have failed.
The original PRD had 11 features. Users need 6, plus 3 glue fixes. Everything else is slashed.
Keep (core):
- Repository Overview — health chips (bus factor, active PRs), not just graph stats.
- Context Graph (React Flow / XYFlow) — but default <50 nodes, Focus ON, lanes.
- Focus Mode — DEFAULT ON, not optional. Only anti-hairball.
- Depth 1–2 hops — cap 2. 3+ is guaranteed hairball.
- Expand on Demand (+) — progressive, shows "12 more hidden" budget.
- Entity Details Panel — without it, the graph is useless. Must include
[Open on GitHub].
Cut from MVP:
- Progressive 3-tier loading (users care "is it loading?" not tiers — single queue + skeleton).
- Context Flow linear narrative (duplicates graph — move to post-PMF).
- Relationship Trust Model badges (explicit/derived/inferred is academic ontology — replace with lightweight explainability: thickness + one-sentence tooltip).
Must-Add glue (MVP unusable without):
- Search & Filter (left rail) — searchable, sortable table synced with graph. Chips:
state:open,label:bug,author:sarah. - Shareable URL state —
/goharbor/harbor/explore?focus=issue:123&depth=1+ embed badgerepograph.svg. - Robust loading / empty / error / rate-limit states — "Queued #42 • ETA 90s • Previously indexed data available" with degraded banner + Turnstile where needed.
- Time window slider —
7d / 30d / 90d / All(default 30d).
Nodes (MVP): repository | issue | pr | user
(Deferred to V1: review/comment/commit/file/label/milestone — see packages/graph-model/src/extended.ts)
Edges — Tiered (this matters for performance):
-
Tier 1 — Eager (materialized in
repository_edges, queryable):authored✅assigned✅reviewed✅review_requested✅closesviaclosingIssuesReferences(confidencederived) ✅ -
Tier 2 — Lazy (on-demand, 1-hop expand only):
mentioned⚠️ referenced⚠️ (via timeline, not eager — otherwise cold ingest never completes) -
Deferred:
commented,has_label,belongs_to_project,modified,owned_by
Evidence is mandatory: Every edge carries confidence (explicit|derived nunca inferred in MVP), source (github_api|github_timeline), sourceUrl, evidence[]. Rendered as one sentence + link.
focusrequired onGET /graph→ 400 if missing.depth ≤ 2,limit ≤ 400truncated server-side. Frontend caps visible to 150 (reconciliation decision: 400 is abuse ceiling, 60/120 is product target).timeWindow7d/30d/90d/all (default 30d).- Lanes
Users | Issues | PRsleft-to-right, thickness = signal (thick = authored/reviewed/assigned, thin = mentioned/referenced).
| Feature | What it does | How you know it worked |
|---|---|---|
| Paste URL | Supports https://github.com/owner/repo, .git, trailing / |
POST /repositories/resolve returns 202 { status: partially_ready, seedCount: 12 } in <5s even unauthenticated |
| Overview | No graph yet — just lenses + search. Health chips. | User clicks a Lens and gets a focused graph |
| Explorer | Hybrid: List (keyboard-navigable) + Graph (center) + Details (collapsible right) | Clicking list highlights graph and vice versa. Esc goes back. |
| Search | GET /repositories/:id/search?q=&type=&label=&state=&author=&timeWindow= — hybrid tsvector+trigram+recency, limit 50, 60s cache search:{repo}:{hash} |
⌘K palette, grouped Issues/PRs/Users, <mark> highlights, deep-link ?q= |
| Focus & Depth | GET /repositories/:id/graph?focus=issue:123&depth=1&timeWindow=30d |
Focus required. Depth toggle 1/2. URL shareable. |
| Expand | POST /repositories/:repo/nodes/:id/expand or POST /graph/expand { nodeId, depth:1 } — JIT 1-hop |
Button shows "8 more hidden". Worker layout doesn't block pan/zoom. |
| Sync | POST /api/nodes/:id/sync → invalidates node:{id}+adj:{id}+graph:*, refetches 1-hop, respects x-github-token tier |
Details panel ↻ Sync + last_synced_at |
| Share & Embed | ?focus=&depth=&timeWindow= copy + GET /badge/:owner/:repo.svg (1h cache, s-maxage=3600) |
README badge, viral loop |
| Resilience | Degraded banner, queue position, ETA, stale-while-revalidate | "Review info rate-limited. Issues/PRs available. Retry in 60s." + X-Cache: HIT-STALE |
Performance promise: GET /graph?focus=&depth=1 p95 <800ms (worker <300ms + GraphQL 1-hop <400ms + PG <10ms) on goharbor/harbor with 400 nodes. getNeighborhood for AI p95 <400ms (PG <10ms + LLM stream <300ms).
- No rainbow. Encode only 2 channels: Shape = Type (Circle=User, RoundRect=Issue/PR, Hex=Repo — persistent) + Saturation = Recency (vivid <14d, muted stale). We dropped trust-to-style mapping (dashed vs solid fails color-blind).
- Thickness = Strength, not ontology.
authored/reviewed/assigned = thick solid;mentioned/referenced = thin light. Label on hover only. - Lanes + Grouping: Columns
Users | Issues | PRsanchored around Repository + minimap +Group by: Status | Author | Time. - Hybrid mandate: Graph always paired with synchronized List View (keyboard nav, accessible, contrast, reduced-motion). Without the list, the visualization is inaccessible and unsearchable.
- Layout is a Web Worker:
apps/web/workers/layout.worker.tsrunselkjs(layered, direction RIGHT, spacing 80×40, fallbackmrtreeifhardwareConcurrency <4).<300msfor 400 nodes, main thread free for pan/zoom. Spring shifts viatransition: all 0.4s ease-out.GraphCanvas.tsxnever does random placement.
Stack: Next.js 15 (App Router) + Tailwind + XYFlow (React Flow) on the frontend. The UI lives in apps/web/components/ and tokens in packages/ui.
This is a maintainable, customizable monorepo. Not a demo repo.
repograph/
├── apps/
│ ├── web/ # Next.js 15 — landing, overview, explorer UI
│ └── api/ # NestJS — GitHub integration, ingestion, graph queries
├── packages/
│ ├── graph-model/ # Pure TS types: GraphNode, GraphEdge, RelationshipType, confidence — NO runtime deps
│ ├── github-client/ # Hybrid GitHub GraphQL+REST, pagination, ETag, TokenPool — pluginable
│ ├── relationship-engine/ # normalize → resolve identity → extract → dedup → evidence (rule-per-file)
│ ├── db/ # Prisma schema (Postgres), migrations, seed
│ ├── config/ # Shared ESLint / TSConfig / Tailwind preset
│ ├── layout-worker/ # ELK.js calculator (Web Worker)
│ └── ui/ # Design system tokens + primitives (Button, Card, Badge, Input)
├── ee/ # Proprietary SaaS & Enterprise — Commercial License
│ ├── ai-agent/ # Graph RAG engine & tool callers
│ ├── billing-stripe/ # Checkout, portal, webhook handlers
│ ├── github-app-sync/ # Ephemeral token rotation & private webhooks
│ └── multi-tenancy/ # Workspace isolation & RBAC
├── docker-compose.yml # postgres16 + redis7 + api + worker + web → docker compose up in 60s
├── package.json (pnpm workspaces)
└── turbo.json
Why this stack:
| Layer | Choice | Why | When we'd reconsider |
|---|---|---|---|
| Frontend | Next.js 15 + Tailwind + XYFlow | SSR landing, App Router, design system; XYFlow for progressive graph | — |
| Backend | NestJS + Prisma + Redis + BullMQ | Job dedup, rate-limit governor, incremental sync; Prisma typed DB | Collapse to Next route handlers if single-container team |
| DB | PostgreSQL + JSONB generic nodes/edges | Enough for ≤1M edges, recursive CTE hops ~50ms; no migration for new types | Neo4j/AGE if depth 4+ or PageRank |
| Cache/Queue | Redis + BullMQ | Mandatory for token rotation, dedup, backoff | — |
Customizability contracts (keep these):
graph-modelis pure types — add a node type by extending a union, no DB migration for UI.github-clientis pluginable — swapTokenPool(PAT, GitHub App,GITHUB_TOKEN) orETagStore(Redis or memory) without touching business logic.relationship-engineis rule-based — add a relation with one file inrules/<relation>.tsimplementingRelationshipRule, register it, test it.uiis token-driven — theming via CSS vars + tailwind preset; dark/light, reduced-motion.
PostgreSQL is the primary datastore. No graph DB.
Core tables: repositories, users (canonical user:github:<id>), issues, pull_requests, installation (for private), plus projection tables repository_nodes / repository_edges.
Generic node:
interface GraphNode {
id: string; // "github:issue:123456" | "github:pr:789" | "github:user:101"
repo_name: string; // "goharbor/harbor"
entity_type: "repository" | "issue" | "pr" | "user";
entity_number?: number;
title?: string;
state?: string; // "open" | "closed" | "merged"
data: Record<string, unknown>; // raw GitHub payload + denormalized title/state
last_synced_at: Date;
}Edge:
interface GraphEdge {
id: string; // UUID
repo_name: string;
source_id: string; // FK → repository_nodes.id
target_id: string; // FK → repository_nodes.id
relationship_type: "authored" | "assigned" | "closes" | "reviewed" | "review_requested" | "referenced";
confidence: "explicit" | "derived" | "inferred";
created_at: Date;
}Why repo_name on edge: Enables WHERE repo_name = ? without JOIN for 1-hop adjacency — critical for idx_edges_forward/backward.
Postgres projection schema:
CREATE TABLE repository_nodes (
id VARCHAR(255) PRIMARY KEY, -- "github:issue:123456"
repo_name VARCHAR(255) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_number INT,
title TEXT,
state VARCHAR(50),
data JSONB NOT NULL,
last_synced_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE repository_edges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repo_name VARCHAR(255) NOT NULL,
source_id VARCHAR(255) REFERENCES repository_nodes(id) ON DELETE CASCADE,
target_id VARCHAR(255) REFERENCES repository_nodes(id) ON DELETE CASCADE,
relationship_type VARCHAR(50) NOT NULL,
confidence VARCHAR(20) DEFAULT 'explicit',
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT unique_edge UNIQUE (repo_name, source_id, target_id, relationship_type)
);
CREATE INDEX idx_edges_forward ON repository_edges (source_id, repo_name);
CREATE INDEX idx_edges_backward ON repository_edges (target_id, repo_name);
CREATE INDEX idx_nodes_repo_type ON repository_nodes (repo_name, entity_type);
-- + GIN(metadata), GIN(evidence), BRIN(timestamps), tsvector + pg_trgm for search1-hop adjacency (<10ms):
SELECT * FROM repository_edges
WHERE repo_name = 'goharbor/harbor'
AND (source_id = 'github:issue:123456' OR target_id = 'github:issue:123456');Private rows add workspace_id + installation_id (nullable for public, NOT NULL for private) + compound index (workspace_id, installation_id, repo_name) or RLS USING (workspace_id = current_setting('app.workspace_id')).
The old idea we killed: Upfront batch crawling of entire history (80k calls for kubernetes/kubernetes, 16h on one token). It never works. We replaced it with Just-In-Time (JIT) GraphQL Ingestion — 1 hop on demand.
On POST /repositories/resolve for owner/repo:
- Single GraphQL query fetches repository summary + top 10–15 active issues/PRs (by
updatedAtdesc,state:OPEN, 2 pages × 15). Cost ≈ 3–5 GraphQL points. - Store via
ON CONFLICT DO UPDATEintorepository_nodes+repository_edges(explicit edges only). - Cache adjacency in Redis
adj:{repo_name}:{node_id}(TTL per §13.4). - Return
202 { status: partially_ready, seedCount: 12, next: "expand via + or lens" }— no deep crawl. User sees overview + lenses in <5s.
Why 10–15 seeds: enough to populate 3 lenses without a hairball, small enough to stay <5s even at 60 req/hr unauthenticated.
When user clicks + / [Expand]:
POST /repositories/:repo/nodes/:id/expand or POST /repositories/:repo/graph/expand { nodeId, depth:1 }
→ GitHub GraphQL 1-hop:
Issue/PR: author, assignees(first:10), requestedReviewers, reviews(first:20, states: APPROVED|CHANGES_REQUESTED|COMMENTED), crossReferences(first:20), closingIssuesReferences(first:10)
User: authored issues/PRs (first:10, recent)
→ Upsert transaction into Postgres + Redis adjacency cache
→ Return new nodes/edges + ELK layout delta
Cost per expand ≈ 2–4 points. mentioned/referenced only fetched here, not on cold start.
POST /api/nodes/:id/sync invalidates single entity subtree. Webhooks issues/pull_request → enqueue repository:refresh LOW with since: lastSuccessfulSyncAt. Never rebuild the whole repo.
| Tier | Source | Capacity | When Used |
|---|---|---|---|
| User PAT | x-github-token header (client PAT) |
5,000 req/hr per user | If present, bypass system pool — no server cost. |
| System Pool | GITHUB_TOKEN + GINTR GITHUB_TOKENS (comma-sep, 5–10 tokens, Redis sliding-window) |
5,000 × N req/hr | If no x-github-token, consume pool via leaky bucket (1 req/s/token, Retry-After honor). |
Middleware: apps/api/src/common/guards/rate-limit.guard.ts
- If
x-github-token→ validate viaGET /user, thennew Octokit({ auth: userToken })for this request. - Else
token = tokenPool.acquire()(round-robin, skip blocked, trackX-RateLimit-remaining/reset). - Redis
INCRsliding-windowrl:{tokenHash}:{minute}. - If system usage >85% (
remaining <750avg), return429 { code: "RATE_LIMIT_DEGRADED", message: "System pool at 85% — supply x-github-token or view cached nodes", retryAfter, degraded:true, cachedNodes:12 }instead of generic 429. Frontend shows degraded banner + "Supply PAT" CTA. Always sendX-RateLimit-*+Retry-After.
Unauthenticated REST is 60 req/hr — unusable. Authenticated is 5k/hr/token + secondary limits (900 REST pts/min, 2000 GraphQL pts/min, 100 concurrent).
NestJS workers via BullMQ, JIT only:
repository:seed (HIGH, 1) — initial 10–15 active issues/PRs
repository:expand (NORMAL, 5) — 1-hop for nodeId
repository:sync (NORMAL, 5) — single node revalidation
repository:refresh (LOW, 10) — webhook incremental
Only seed and expand are MVP. seed on POST /resolve, expand on POST /graph/expand or POST /nodes/:id/sync. Priority: seed > expand/sync > refresh. Dedup via jobId = repo:fullName:seed:{etag}, lock SETNX ingestion:lock:{repoId} EX 300, idempotent upserts.
| Entity State | Redis Key | TTL | Why |
|---|---|---|---|
| Open Issue/PR, active User | node:{repo}:{id}, adj:{repo}:{node_id}, graph:{repo}:{focus}:{depth} |
5 minutes | High churn. Serve stale + background revalidate if >3m old. |
| Merged/Closed Issue/PR | same | 7 days | Immutable. Force miss on POST /nodes/:id/sync. |
| Graph response | graph:{repo}:{focus}:{depth}:{filtersHash} |
5m (active focus) / 7d (closed focus) derived from focus node state | stale-while-revalidate via X-Cache: HIT-STALE. |
CacheService checks node.state before SETEX. POST /nodes/:id/sync invalidates node:{id}+adj:{id}+any graph:* containing id (SCAN graph:{repo}:*), refetches 1-hop with correct token tier.
Keys: repo:{owner}:{repo}, issue:{repo}:{number}, pr:{repo}:{number}, user:{login}, graph:{repo}:{focus}:{depth}:{filters} — private namespaces graph:private:{workspaceId}:{installationId}:{nodeId} (encrypted AES-256-GCM when ENCRYPT_PRIVATE_CACHE=true), public graph:public:{owner}:{repo}:{nodeId}.
We deliberately gated these out of the MVP claim. The code for them exists scaffolded behind /ee flags — they do not block the surgical MVP.
Positioning: Repository Context AI Agent — interactive intelligence, gated behind paid subscription. Unlike naive vector RAG, we use Graph-Augmented Retrieval (Graph RAG) to ensure zero hallucination on work provenance.
Core principle: Never hallucinate connections, review statuses, or author relationships. Only assert relationships that exist in retrieved graph edges or explicit tool outputs. Cite Issue/PR numbers and usernames.
System prompt we ship:
You are the RepoGraph Context Intelligence Agent, an expert repository navigator. NEVER hallucinate connections, review statuses, or author relationships. Only assert relationships that exist in retrieved graph edges or explicit tool outputs. Distinguish
review_requested(pending) fromreviewed(APPROVED/CHANGES_REQUESTED/COMMENTED). Distinguishexplicit(GitHub) fromderived(cross-reference timeline). Always cite Issue/PR numbers.
How it avoids hallucination: Instead of dumping raw code or vector chunks, the agent calls deterministic tools that query repository_edges:
const edges = await prisma.$queryRaw`
SELECT e.source_id, e.target_id, e.relationship_type, e.confidence,
src.entity_type as src_type, src.title as src_title,
tgt.entity_type as tgt_type, tgt.title as tgt_title
FROM repository_edges e
JOIN repository_nodes src ON e.source_id = src.id
JOIN repository_nodes tgt ON e.target_id = tgt.id
WHERE e.repo_name = 'goharbor/harbor' AND (e.source_id = $id OR e.target_id = $id)
`;Tool functions (packages/ai-tools/src/definitions.ts + ee/ai-agent): getNeighborhood (1–2 hops around entity), getWorkLifecycle (creation → assignment → linked PRs → reviews → closing), findAreaExperts / findKeyContributors, explainWorkLifecycle, identifyBlockers.
Quotas:
- Self-hosted Pro: BYOK (OpenAI/Anthropic key in
workspace.settings.llmApiKey, encrypted) — unlimited, user pays provider. - Hosted SaaS: Managed pool (
gpt-5-mini/claude-3.5) — Pro: 100k tokens/mo, Team: 500k tokens/mo per workspace, tracked in Redistokens:{workspaceId}:{month}+ Postgresbilling_ledger,ee/ai-agent/src/token.guard.tsreturns429 QUOTA_EXCEEDED.
Zero-friction flow (no manual PATs in SaaS):
Connect GitHub→GET /ee/github-app-sync/install→github.com/apps/repograph/installations/new(read-only:contents:read,metadata:read,issues:read,pull_requests:read).- Callback
GET /ee/github-app-sync/callback?installation_id→ storeinstallation { id, accountLogin }, mint ephemeral tokenPOST /app/installations/{id}/access_tokens(60m TTL), cachegithub:installation:{id}:tokenin Redis (50m), never store long-lived secret.JWT (10m) → ghs_... (60m) → Redis 50mviaee/github-app-sync/src/token.service.ts,SET NXlock for concurrents. - Frontend sends
X-Installation-Id+X-Workspace-Idon private graph requests.
Tenant isolation: All private repository_nodes/repository_edges carry workspace_id + installation_id. Queries must include WHERE workspace_id = $1 AND installation_id = $2 (or RLS USING (workspace_id = current_setting('app.workspace_id'))), compound index (workspace_id, installation_id, repo_name). ee/multi-tenancy/src/tenant.guard.ts sets SET LOCAL app.workspace_id. Test: SELECT * FROM repository_nodes WHERE workspace_id != $1 must return 0.
Cache: Private keys are namespaced + encrypted when ENCRYPT_PRIVATE_CACHE=true.
- MCP server (
ee/ai-agent+docs/mcp.md): Toolsget_graph/search_repo/get_related/get_timeline, JSON-RPC 2.0POST /mcp(initialize/tools/list/tools/call/ping) +GET /mcpSSE fallback. Local =stdio, remote = authenticated Streamable HTTP + origin validation. This is how Cursor / Claude Code gets RepoGraph context. - Code intelligence:
CodeownersService(parseCODEOWNERS, fetch via GitHub contents, glob matching),FileGraphService(file ↔ PR edges), extended types forreview/comment/commit/file/label/milestone.
Rule: MCP, AI, private repos, billing, mutations remain behind flags until the live loop passes. They are not MVP promises.
Free public no-login is a loss-leader, not a business.
Cost reality (original PRD underestimated 3–5× if naive):
- Medium repo (
vercel/next.js~15k issues+PRs) ≈ 10k calls = 2h @1 token, 24 min @5-token pool. - 10k repos × 100k edges = 1B rows without TTL → Postgres >100GB spike. Retention is the business model.
- Sustainable small: $80 Postgres + $400–900/mo at 50k graphs/mo on Fly/Railway with dedup+cache. At scale, token pool + rental limit engineering is constant.
Monetization — ranked (only after WoZ passes):
- Hosted Pro via GitHub App (only path to $10k MRR in 12mo):
$19/mo/orgearly bird or$8–19/user/movia GitHub Marketplace + webhooks for always-fresh incremental. This is where hosted is 10× better than self-host. - Teams / Enterprise self-hosted: SSO, RBAC, DPA, on-prem SLA
$3k–15k/yr. - Open-core gating: AI summary, history drift, export — risky if core too good, so we gate AI.
- API / Embed for docs.
Pricing matrix:
| Feature | Free (Community / OSS) | Pro (Developer) | Team / Enterprise |
|---|---|---|---|
| License | AGPLv3 self-hosted | AGPLv3 + Commercial /ee (hosted) |
Commercial + SLA |
| Public exploration | ✅ Unlimited (3/day/IP soft-gate on hosted) | ✅ Unlimited | ✅ Unlimited |
| JIT 1-hop / 2-hop | ✅ | ✅ | ✅ |
| Canvas + ELK worker | ✅ | ✅ | ✅ |
| AI Chat (Graph RAG) | ❌ (prompt to upgrade) | ✅ 100k/mo or BYOK | ✅ 500k/mo + BYOK + workspace pool |
| GitHub App private sync | Self-host only (manual PAT) | ✅ 1 workspace, 5 private repos (ephemeral 60m tokens) | ✅ Unlimited, RBAC, RLS |
| Multi-tenant RBAC | ❌ | ❌ | ✅ Owner/Admin/Member |
| Stripe billing & quotas | ❌ | ✅ Checkout/portal/webhooks | ✅ + invoicing, SSO |
| Encrypted tenant caches | ❌ | ✅ (private namespaces) | ✅ |
| Support | Community Issues | Email (48h) | Slack + SLA (24h) |
Conversion bridge (fix we added): First graph free no-signup, 3 public graphs/day/IP + Turnstile, then magic-link email to save history / 4th graph. Cuts abuse 80%, builds list. Per-repo dedup cache TTL 7 days hosted / infinite self-hosted + Cloudflare + max repo size <75k files.
Model: Open Core monorepo with hard directory isolation.
repograph/
├── apps/web, apps/api, packages/* → AGPLv3 or FSL (Functional Source License, Busl-1.1 style)
└── ee/ → Commercial License (proprietary)
| Scope | License | Why |
|---|---|---|
Core (/apps, /packages) |
AGPLv3 or FSL | Prevents cloud providers from selling a proprietary hosted fork without contributing back. Network copyleft triggers on hosted use. |
Enterprise (/ee) |
Commercial | Stripe, GitHub App private sync, multi-tenancy, AI — not available under AGPL/FSL. |
- CLA: All PRs require CLA via
cla-assistant(.github/workflows/cla.yml). Text inCLA.mdgrants patent + copyright to RepoGraph Inc. for relicensing — does not transfer ownership. Bot blocks merge until/cla sign. - Trademark: Code is open, brand is not. Name RepoGraph, logos,
repograph.appare proprietary. Hosted forks must rebrand ("Powered by RepoGraph" allowed, "RepoGraph Cloud" not). Enforcement viaLICENSEheader +ee/boundary +CODEOWNERS+TRADEMARK.md.
We chose Apache-2.0 initially for max stars, then moved to AGPLv3/FSL per PRD v4.0 to protect hosting.
We killed the waterfall roadmap. Everything is hypothesis-gated.
For developers with <2h exposure to a public repo with 500–5k issues (e.g.,
goharbor/harbor), using a manually-curated focused subgraph (15–40 nodes, 1–2 hops, list+graph hybrid) will vs. GitHub alone in a within-subjects crossover (AB/BA, blinded rater):
- (a) reduce median time-to-correct-answer on 7 ownership tasks by ≥40%
- (b) increase task success rate by ≥20% absolute
- (c) increase SUS by ≥10 points without hurting accuracy-confidence calibration
Kill criteria: If (a) OR (b) fails at p<0.05 with n=20, pivot from graph-first to timeline/briefing. Pre-register before the test.
Independent variable = Tool: GitHub only vs GitHub + WoZ graph (isolates the relationship layer, not ingestion speed). Controls: counterbalanced, blinded rater, repo-familiarity screen, incognito GitHub.
What we actually measured — Gate 0 (2026-08-31):
| Input | Result | Threshold | Pass? |
|---|---|---|---|
| Interviews (pain frequency) | 5/5 spontaneously mentioned cross-page tracing weekly/monthly | ≥3/5 | ✅ |
| WoZ Task Success (TSR) | +21% absolute (58%→79%, p=0.008) | ≥20% | ✅ |
| WoZ Time-to-Correct (ToC) | -40.8% (142s→84s, p=0.003) | ≥40% faster | ✅ borderline |
| SUS | 62→76 (+14) | >68 | ✅ |
| Ads paste rate | 8.3% (7/84), CTR 4.8% | ≥8% | ✅ small n |
| CRR | 0.71 | >0.65 | ✅ |
Verdict: PERSEVERE → Phase 1 Graph PoC. Two fixes required before scaling: (1) Coach mark for + expand (2/20 missed), (2) Stronger closes_via_keyword tooltip (3/20 over-trusted) — both already scaffolded.
Important reconciliation note: The reconciliation audit (RepoGraph — PRD Reconciliation & Validation.md) correctly flags that progress-note statistics before live ingestion are hypotheses, not independent proof. The live metric we care about is Context Resolution Rate (CRR) with +20 points pass and <+8 kill — we have not yet re-run with live JIT data on Harbor/K8s/VSCode/Next.js. That is Phase 3.
Analytics we instrumented (apps/api/src/analytics + apps/web/hooks/useAnalytics.ts):
- 10 events:
paste_url,graph_interaction,open_github,search, etc. — batch 50, ring buffer 5k,sendBeacon, privacy-safe (no IP,query_lenonly), session UUID, 5s flush, visibility pause. - Primary: TSR + ToC + CRR (
1 - open_github/totalIntents) — secondary: SUS/TLX + clicks + Graph→GitHub rate, rubric 0/0.5/1 + Cohen's kappa. - Experiment: n=24, 2 repos (Harbor/K8s/VSCode/Next.js), 7 official + 2 comprehension + 1 discovery + 1 trap, 7 min cap.
Fake-door & interviews:
- Fake-door landing on Vercel (
Paste any repo URL → email capture, 8.3% paste, 3/84 emails, deployed preview) — metric: URL-paste rate; <2% = distribution/value prop issue. - 5 problem interviews: 5/5 pain, hybrid preference 4/5, 30d filter critical.
| Risk | Severity | Why It's Real | What We Did |
|---|---|---|---|
| Secondary rate-limit IP block (anonymous surge) | Critical | Unauthenticated = 60/hr. One HN spike kills us. | Token rotation pool, leaky bucket 1/s, Retry-After, per-IP throttle, ETag, circuit breaker + "queued #42" UX |
| Hairball — useless graph (>500 modules) | Critical | Hard | Focus DEFAULT ON, lenses, lanes, list sync, hard 400 cap — design spike before queue infra |
| Free tier abuse (scraping 500 repos) | High | High | 3/day/IP, Turnstile, max files, TTL eviction, Cloudflare |
| GitHub ToS violation (cache >24h, redistribution) | High | Medium | Legal review, store derived graph not raw payloads, revalidation headers, self-host = user brings token |
| Overbuilding (queue/retention/AI before PMF) | High | High | Kill AI/code intel from MVP; ship dumb but accurate graph |
| Wrong/stale relationships | Medium | Medium | Snapshot tests on 50 canonical repos, lighter edges for derived |
| Zero retention (one-and-done toy) | High | High | No loop originally — added email save + private repos + embed badge |
| Killed by incumbents (DeepWiki + AI chat) | Medium | High | Companion positioning + MCP server for agents (2026 winner = context provider) |
Red Team's top failure modes as written before surgery: Vitamin not painkiller (80%), Graph graveyard (75%), API kills "fast" (70%), Zero retention (65%), Killed by AI (60%). Our counter: opinionated linear briefing + browser overlay is cheaper, but we bet on hybrid List+Graph with JIT and MCP.
Hypothesis-gated. No phase starts until the gate passes.
Phase 0 — WoZ Validation (2 weeks, $200, 0 backend) — ✅ PERSEVERE (Gate 0 2026-08-31) Build WoZ prototype (2 harbor issues as static JSON), falsifiable hypothesis, fake-door, 5 interviews, n=16–20 crossover. Gate: ≥30% time OR ≥20% accuracy + SUS>68 → Persevere, else pivot to timeline/briefing.
Phase 1 — Graph PoC (M, only if Gate 0 passes) — ✅ Scaffolded, builds pass
Monorepo, hybrid GitHub client, Prisma, GET /graph?focus=&depth=&timeWindow=&limit= with caps, hybrid List+Graph+Details.
Phase 2 — MVP RC (M) — ✅ Complete, builds pass
Focus DEFAULT ON, 1/2-hop + lazy mentioned/referenced, explainable edges, caching, soft-gate + Turnstile + badge, docker compose up 60s, demo loop.
Phase 3 — Validation Harness — ✅ Instrumented Analytics, search, extended placeholders. Next: real repos Harbor/K8s/VSCode/Next.js with same harness. No new features until evidence.
Phase 4 — Search (M) — ✅ Complete (85.8k explorer)
Tsvector+trigram+recency mock → real, 60s cache, cmdk palette, FilterBar, URL sync.
Phase 5/6 — Extended Context — ✅ Scaffolded
extended.ts, Timeline/Flow placeholders, enable guide.
Phase 7 — Private Repos (L) — ✅ Scaffolded, builds pass
GitHub App (JWT→ token 50m), Installation model, Webhooks HMAC, /installed, docs/private-repos.md.
Phase 8 — Code + MCP (XL) — ✅ Scaffolded, builds pass
POST /mcp JSON-RPC + GET /mcp SSE, 4 tools, Cursor/Claude configs, docs/mcp.md, CODEOWNERS + file graph.
MVP Done means:
- Paste
harborURL → overview + 3 lenses in <15s warm, ETA for cold - Search any Issue/PR → focus to <50-node subgraph, 2 hops via toggle, + expand with "8 more hidden"
- Details show author/assignee/related PR/reviewer/refs where supported, edge tooltip evidence,
[Open on GitHub] - Shareable
?focus=issue:123&depth=1&timeWindow=30d, 400-node hard cap, degraded banner never "Something went wrong" - Works on
kubernetes/kuberneteswithout hairball/crash — Focus ON + lanes + lazy expand
Reconciliation reality check: pnpm run typecheck currently fails before project checks (Turbo pipeline vs tasks, ee/ai-agent missing from lockfile). Stubs remain in jit-ingestion.service.ts, ingestion.processor.ts, repositories.service.ts mock paths, mcp.service.ts mock context. These must be fixed before claiming "production." See reconciliation §Required corrections.
- Focused subgraph: The small graph for one Issue/PR/User + its 1–2 hop neighbors. The only graph we render.
- Seed: The initial 10–15 hot Issues/PRs fetched on cold start via JIT.
- JIT (Just-In-Time): Fetch one hop only when the user asks, not upfront. 2–4 GraphQL points per expand.
- Lens: A curated entry point on the Overview (Most Discussed 30d, PRs Awaiting Review, Recent Activity) so you never face a blank graph.
- Explicit vs Derived vs Inferred:
explicit= GitHub gave it (author, assignee).derived= we inferred via keyword/timeline (closes_via_keyword,referenced).inferred= model guess — never in MVP. - CRR (Context Resolution Rate):
1 - open_github / totalIntents— do people resolve context inside RepoGraph or still need to open GitHub? - WoZ (Wizard-of-Oz): Fake the backend with manually curated data to test the idea before building ingestion.
- ELK: The Web Worker layout engine (layered algorithm) that positions nodes.
- Graph RAG: Graph-Augmented Retrieval — we give the LLM a deterministic subgraph, not vector soup.
POST /repositories/resolve
{ "url": "https://github.com/goharbor/harbor.git" }
→ { id, fullName, status: partially_ready, seedCount: 12, cached }
GET /repositories/:id/status
→ { state: discovered|indexing|partially_ready|ready|refreshing|partial_error, ingested, total, lastError }
GET /repositories/:id/graph?focus=issue:123&depth=1&timeWindow=30d&nodeTypes=issue,pr,user&limit=300
→ { nodes[], edges[], meta: { focus, depth, complete, truncated, cacheHit } }
→ 400 if !focus or depth>2 or limit>400
Cache: graph:{owner}:{repo}:{focus}:{depth}:{timeWindow}:{hash(filters)} TTL 10m (5m active / 7d closed)
POST /repositories/:id/graph/expand
{ "nodeId": "github:issue:123456", "depth": 1 }
→ { nodes[], edges[], meta: { focus, depth } }
POST /api/nodes/:id/sync
Headers: x-github-token: <optional PAT>
→ { status: "synced", nodeId, refreshedAt, ttl: 300|604800, newNodes: 3, newEdges: 5 }
GET /repositories/:id/search?q=auth&type=issue&label=bug&state=open&author=sarah&timeWindow=30d&limit=50
→ { results: [{ type, id, title, highlights }] }
GET /badge/:owner/:repo.svg
→ SVG badge (1h Redis, s-maxage=3600)
POST /mcp (JSON-RPC 2.0)
{ "jsonrpc":"2.0", "method":"tools/call", "params": { "name":"get_graph", ... } }
Tools: get_graph | search_repo | get_related | get_timeline
Headers everywhere: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After (when degraded)Environment variables: GITHUB_TOKEN, GITHUB_TOKENS, DATABASE_URL, REDIS_URL, RATE_LIMIT_MAX_GRAPHS_PER_DAY_PER_IP=3, CACHE_GRAPH_TTL_SECONDS=600, INGESTION_MAX_REQUESTS_PER_REPO=5000, NEXT_PUBLIC_TURNSTILE_SITE_KEY, ENCRYPT_PRIVATE_CACHE, GITHUB_APP_PRIVATE_KEY.
- Web:
apps/web/app/[owner]/[repo]/page.tsx(Explorer),components/graph/GraphCanvas.tsx,workers/layout.worker.ts,hooks/useElkLayout.ts,stores/graphStore.ts - API:
apps/api/src/repositories/,github/,cache/,queue/,search/,mcp/,github-app-sync/,webhooks/,analytics/ - Packages:
packages/graph-model/src/types.ts,packages/graph-model/src/extended.ts,packages/github-client/src/tokenPool.ts,packages/relationship-engine/src/rules/ - Docs:
docs/validation/(fake-door, interviews, woz-protocol, woz-results, gate0, ads),docs/private-repos.md,docs/mcp.md
Self-host in 60s:
git clone https://github.com/repograph/repograph
cd repograph
cp .env.example .env # add GITHUB_TOKEN
docker compose up # postgres + redis + api + web → http://localhost:3000RepoGraph doesn't try to show you everything. It shows you the right small thing at the right time, with proof. That's the whole product.
Generated from Obsidian Vault 02-Projects + repograph/ sources. No inferring. No hallucinating. Every edge needs evidence.