Skip to content

Latest commit

 

History

History
572 lines (420 loc) · 28.5 KB

File metadata and controls

572 lines (420 loc) · 28.5 KB

RepoGraph — Product Requirements Document

Version: 4.0 — AI SaaS (Graph RAG) & COSS Architecture Status: Active Last Updated: 2026-09-01 Source of Truth: This file + 02-Projects/RepoGraph Side Project.md (Validated PRD v2.0) + ARCHITECTURE.md Supersedes: v3.0 JIT + ELK + Projection (retained below, extended with AI/COSS)


8. MVP Definition (Revised — Progressive Context Layer)

RepoGraph is NOT a crawler or code visualizer. It is a progressive context layer over GitHub that answers: “Who is involved, what is connected, what should I look at next?” within 1–2 hops of focus.

MVP Promise:

Any public repo can be focused in <15s to a readable subgraph (<15 nodes seeded, <50 total) that answers who owns what and which PRs relate. Full repo graph is never attempted.

Supported Nodes (MVP): repository | issue | pr | user Supported Edges (Tier 1 eager, Tier 2 lazy): authored | assigned | closes | reviewed | review_requested | mentioned | referenced (see §8.1)

UX Guardrails:

  • Focus DEFAULT ON — canvas never renders unfiltered hairball
  • focus required on GET /graph → 400 if missing
  • depth ≤2, limit ≤400 truncated server-side
  • timeWindow 7d/30d/90d/all (default 30d)
  • Lanes Users | Issues | PRs left-to-right, thickness = signal

8.1 Supported Nodes & Edges (Slimmed)

Nodes: repository | issue | pr | user (+ review/comment/commit/file deferred to V1, see packages/graph-model/src/extended.ts)

Edges — Tiered:

  • Tier 1 eager (materialized in repository_edges): authoredassignedreviewedreview_requestedcloses (via closingIssuesReferences, confidence derived) ✅
  • Tier 2 lazy (on-demand, 1-hop expand only): mentioned ⚠️ referenced ⚠️ (via timeline, not eager)
  • Deferred: commented, has_label, belongs_to_project, modified, owned_by

Experience Spec (replaces PRD §10–16):

Landing: [github.com/owner/repo] [Explore] + 1-click demos → POST /repositories/resolve → JIT seed
Overview (no graph yet): Health chips + 3 Lenses [Most Discussed 30d] [PRs Awaiting Review] [Recent Activity] + Search
Graph (on lens/search/focus): Hybrid List (left) + Graph (center, Focus ON, <15 seeded → <50 total) + Details (right)
  Filters: Time 7/30/90/All (default 30d) • Status • Node type • Depth [1 hop][2 hops] (default 1) • + Expand per node
  Edge hover: "Authored: Sarah created Issue #123 Aug 20 [View]" — one sentence + sourceUrl
  Shareable: ?focus=issue:123&depth=1&timeWindow=30d → copy
Errors: degraded banner "Review info rate-limited. Issues/PRs available." never "Something went wrong"

28. Ingestion & Scaling Pipeline (Revised — JIT)

Anti-Pattern Replaced: Upfront batch crawling of entire repo history (80k calls for kubernetes/kubernetes, 16h @ 1 token) → Just-In-Time (JIT) GraphQL Ingestion (1-hop on demand).

28.1 Cold Start (No Historical Crawl)

On initial POST /repositories/resolve for owner/repo:

  1. Single GraphQL query fetches repository summary metadata + top 10–15 active issues/PRs as initial canvas seeds (by updatedAt desc, state: OPEN, 2 pages × 15). Cost ≈ 3–5 GraphQL points.
  2. Store seeds via ON CONFLICT DO UPDATE into repository_nodes + repository_edges (explicit edges only).
  3. Cache adjacency list in Redis adj:{repo_name}:{node_id} TTL per §30.
  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 hairball, small enough to stay <5s even at 60 req/hr unauthenticated, large enough to validate “who is involved”.

28.2 1-Hop Expand (On-Demand)

When user clicks + / [Expand] on any node:

POST /repositories/:repo/nodes/:id/expand  or  POST /repositories/:repo/graph/expand { nodeId, depth:1 }
  → GitHub GraphQL 1-hop query:
    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 (nodes + edges) + Redis adjacency cache
  → Return new nodes/edges + ELK layout delta

Cost per expand ≈ 2–4 points. Lazy mentioned/referenced only fetched here, not on cold start.

28.3 Incremental Refresh (Future)

POST /api/nodes/:id/sync invalidates single entity subtree (see §30). Webhooks issues/pull_request → enqueue repository:refresh LOW priority with since: lastSuccessfulSyncAt.


29. Shared Rate-Limit Architecture (Revised — Multi-Tier)

Anti-Pattern Replaced: Single shared system token pool exhausting at 100 users → Multi-Tier Token Strategy (System Pool + User PATs).

29.1 Token Tiers

Tier Source Capacity When Used
User PAT x-github-token header (client-provided Personal Access Token) 5,000 req/hr per user token If present, bypass system pool — request routed through client token. No server cost.
System Pool GITHUB_TOKEN + GINTR GITHUB_TOKENS (comma-separated, 5–10 tokens, Redis sliding-window) 5,000 × N req/hr If no x-github-token, consume system pool via leaky bucket (1 req/s/token, Retry-After honor).

29.2 Middleware

apps/api/src/common/guards/rate-limit.guard.ts (NestJS guard + middleware):

  1. Check x-github-token header — if present, validate via GET /user with that token, then octokit = new Octokit({ auth: userToken }) for this request only.
  2. Else, token = tokenPool.acquire() (round-robin, skip blocked, remaining/resetAt from X-RateLimit headers).
  3. Track usage via Redis INCR sliding-window rl:{tokenHash}:{minute}.
  4. If system usage >85% capacity (remaining < 750 per token avg), return structured 429 { 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” CTAs.

Headers Always: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After (when degraded).


30. Ingestion Queue (Revised — JIT, Not Batch)

Anti-Pattern Replaced: Batch jobs repository:issues, repository:pull-requests, repository:relationships pre-crawling thousands → JIT single-hop jobs only.

NestJS workers via BullMQ still process repository jobs, but only JIT:

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 (POST /api/nodes/:id/sync)
repository:refresh     (LOW, 10) — webhook incremental

Only seed and expand are MVP. seed is triggered on POST /repositories/resolve (cold start). expand is triggered on POST /repositories/:repo/graph/expand or POST /api/nodes/:id/sync.

Priority: seed (HIGH) > expand/sync (NORMAL) > refresh (LOW). Abandoned background scans never run.


31. Priority Queue (Unchanged — 3 Levels)

HIGH    repository seed (metadata + 15 active)
NORMAL  expand / sync (1-hop, user-initiated)
LOW     refresh (webhook incremental)

Viewer’s repo gets higher priority than abandoned scan via jobId = repo:fullName:seed:{etag} deduplication.


32. Cache Strategy (Revised — Split TTL)

Anti-Pattern Replaced: Uniform TTL → Split TTL Caching (5m Active / 7d Closed) + Node Sync.

Entity State Redis Key TTL Rationale
Open Issue/PR, active User node:{repo}:{id}, adj:{repo}:{node_id}, graph:{repo}:{focus}:{depth} 5 minutes High churn, need freshness. On hit, serve stale + background revalidate if >3m old.
Merged/Closed Issue/PR same 7 days Immutable, cache long. On POST /api/nodes/:id/sync, force miss + re-fetch.
Graph response graph:{repo}:{focus}:{depth}:{filtersHash} 5m (active focus) / 7d (closed focus) derived from focus node state See below.

Implementation: CacheService checks node.state before SETEX. If state IN ('closed','merged') TTL 604800 else 300. GET honors stale-while-revalidate via X-Cache: HIT-STALE.

Granular Revalidation: POST /api/nodes/:id/sync (see §35.1) invalidates node:{id} + adj:{id} + any graph:* containing id (via SCAN graph:{repo}:*), then re-fetches 1-hop via JIT and re-caches with correct TTL.

Cache keys (unchanged):

repo:{owner}:{repo}
issue:{repo}:{number}
pr:{repo}:{number}
user:{login}
graph:{repo}:{focus}:{depth}:{filters}

33. Incremental Refresh (Revised)

Never rebuild whole repo. On POST /api/nodes/:id/sync or webhook:

Invalidate node:{id} + adj:{id}
  ↓
JIT 1-hop fetch for that node only
  ↓
Upsert affected nodes/edges
  ↓
Re-cache adjacency

Later, webhooks enable near-real-time for installed repos.


34. Graph API Design (Revised — Focus Required)

Frontend requests subgraph, not entire repo.

Example:

POST /repositories/:repo/graph/expand  { "nodeId": "github:issue:123456", "depth": 1 }
GET  /repositories/:id/graph?focus=issue:123456&depth=1&timeWindow=30d

Optional filters:

{
  "nodeTypes": ["issue", "pull_request", "user"],
  "relationshipTypes": ["assigned", "authored", "reviewed", "closes"]
}

Response: { nodes[], edges[], meta: { focus, depth, complete, truncated, cacheHit } }


35. Graph Response (Unchanged)

meta.complete false if truncated at 400 nodes, meta.truncated true if sampled.

35.1 Node Sync Endpoint (New)

POST /api/nodes/:id/sync
Headers: x-github-token: <optional PAT>
→ { status: "synced", nodeId, refreshedAt, ttl: 300|604800, newNodes: 3, newEdges: 5 }

Invalidates Redis, re-fetches 1-hop via JIT with correct token tier, upserts Postgres, re-caches. Used by user clicking ↻ Sync on entity details panel and by webhooks.


36. Why Subgraphs (Unchanged)

Avoid GET /repository/all-graph. Use ?focus=&depth= to keep payloads small, queries predictable, rendering fast.


37. Database (Revised — Projection Tables)

PostgreSQL is primary datastore. No graph DB.

Core tables: repositories, users, issues, pull_requests, installation, plus projection tables repository_nodes / repository_edges (see §44).


38. Graph Storage (Revised — Bidirectional Projection)

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.


39. Why Generic Nodes + Typed Domain Data (Unchanged)

Domain tables (issues, pull_requests, users) + projection (repository_nodes/repository_edges) gives both reliable domain queries + flexible graph.


44. PostgreSQL Projection Schema (Revised — Normalized Bidirectional)

CREATE TABLE repository_nodes (
    id VARCHAR(255) PRIMARY KEY, -- format: "github:issue:123456"
    repo_name VARCHAR(255) NOT NULL,
    entity_type VARCHAR(50) NOT NULL, -- 'issue' | 'pr' | 'user' | 'repository'
    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, -- 'authored' | 'assigned' | 'closes' | 'reviewed' | 'review_requested' | 'referenced'
    confidence VARCHAR(20) DEFAULT 'explicit', -- 'explicit' | 'derived' | 'inferred'
    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);

Adjacency Query (1-hop, bidirectional, <10ms):

SELECT * FROM repository_edges
WHERE repo_name = 'goharbor/harbor'
  AND (source_id = 'github:issue:123456' OR target_id = 'github:issue:123456');
-- Uses idx_edges_forward + idx_edges_backward via UNION or OR → BitmapOr

Legacy tables: Existing Node/Edge (with repositoryId FK) remain for backward compat until migration is cut over; new code writes to both, reads from repository_nodes/repository_edges when USE_PROJECTION=true.


45. Entity Details (Expanded)

Details panel now shows last_synced_at + ↻ Sync button → POST /api/nodes/:id/sync (respects x-github-token tier).


46. Relationship Support (Expanded)

Potential relationships now include reviewed with review_state in data (APPROVED etc.), closes via closingIssuesReferences (derived, not closes keyword parsing).


47. Layout Stability & Client Performance (New)

Anti-Pattern Replaced: React Flow jitter / random placement on expansion → Web Worker ELK.js Layout Engine + CSS Spring Shifts.

React Flow must not perform manual or random placement.

Pipeline:

User clicks + Expand
  ↓
Fetch 1-hop via JIT (2–4 points)
  ↓
Pass current graph + new entities to Web Worker (layout.worker.ts)
  ↓
Worker runs elkjs (layered algorithm, direction: RIGHT, spacing: 80x40)
  ↓
Compute new (x,y) without blocking main thread
  ↓
Apply to React Flow state with CSS transitions (transition: all 0.4s ease-out)

Implementation:

  • apps/web/workers/layout.worker.ts — dedicated Worker, import Elk from 'elkjs', elk.layout({ id: "root", layoutOptions: { 'elk.algorithm': 'layered', 'elk.direction': 'RIGHT', 'elk.spacing.nodeNode': '80', 'elk.layered.spacing.nodeNodeBetweenLayers': '40' }, children: nodes, edges })
  • apps/web/hooks/useElkLayout.ts — manages Worker lifecycle, postMessage({ nodes, edges })onmessagesetNodes with position + style: { transition: 'all 0.4s ease-out' }
  • apps/web/components/graph/GraphCanvas.tsx — no manual laneX placement on expansion; initial seed lanes bootstrapped via ELK as well, but with animate: false for first paint to avoid flash.
  • Performance: 400 nodes layout <300ms on worker, main thread free for pan/zoom. On low-end devices, fallback to elk.algorithm: 'mrtree' if navigator.hardwareConcurrency <4.

Zustand Store:

apps/web/stores/graphStore.tsfocusedNodeId, loadedHops: Map<id, depth>, expandedNodes: Set<id>, isLayouting, actions focus(nodeId), expand(nodeId), sync(nodeId) → triggers JIT fetch + worker layout.


100. Final Validation

Validate GET /repositories/:id/graph?focus=...&depth=1 p95 <800ms (worker <300ms + GraphQL 1-hop <400ms + PG <10ms) on goharbor/harbor with 400 nodes.


101. AI Graph Assistant & SaaS Intelligence (V2 / Paid Tier — Pro/Team)

Positioning: Repository Context AI Agent — interactive repository intelligence gated behind paid subscription. Unlike naive vector RAG, RepoGraph uses 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 — RepoGraph Context Intelligence Agent:

You are the RepoGraph Context Intelligence Agent, an expert repository navigator.
Your primary role is to answer questions about GitHub repository work items (Issues, Pull Requests, Reviews, and Ownership) using deterministic graph relationships.

CRITICAL OPERATIONAL RULES:
1. NEVER hallucinate connections, review statuses, or author relationships. 
2. Only assert relationships that exist in the retrieved graph edges or explicit tool outputs.
3. Distinguish between 'review_requested' (pending action) and 'reviewed' (action completed with a state like APPROVED, CHANGES_REQUESTED, or COMMENTED).
4. Clearly distinguish 'explicit' relationships (provided by GitHub) from 'derived' references (extracted from cross-reference timeline events).
5. Always cite specific Issue/PR numbers and usernames when explaining a workflow.
6. When explaining complex workflows, provide a step-by-step chronological summary of the issue-to-merge lifecycle.

101.1 Graph-Augmented Retrieval (Graph RAG)

Instead of passing raw dumps or naive vector chunks to an LLM, the agent queries NestJS Graph tools to extract deterministic relationship subgraphs:

// Tool call → Prisma repository_edges (bidirectional) → deterministic subgraph → LLM context
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 = 'github:issue:123' OR e.target_id = 'github:issue:123')
`;

Ensures authored/closes/reviewed are grounded in repository_edges.confidence (explicit vs derived).

101.2 AI Tool Functions (Function Calling)

Definitions — packages/ai-tools/src/definitions.ts (also mirrored in ee/ai-agent):

import { z } from 'zod';
export const RepoGraphTools = {
  getNeighborhood: {
    description: 'Fetches 1-hop or 2-hop connected graph context around a GitHub entity.',
    parameters: z.object({
      repoOwner: z.string(), repoName: z.string(),
      entityType: z.enum(['issue','pr','user']),
      identifier: z.string(), // "123" or "octocat"
      depth: z.number().min(1).max(2).default(1)
    })
  },
  getWorkLifecycle: {
    description: 'Traces issue/PR from creation → assignment → linked PRs → reviews → closing.',
    parameters: z.object({ repoOwner: z.string(), repoName: z.string(), issueOrPrNumber: z.number() })
  },
  findAreaExperts: {
    description: 'Identifies active authors/dominant reviewers for labels/milestones.',
    parameters: z.object({ repoOwner: z.string(), repoName: z.string(), label: z.string().optional(), limit: z.number().min(1).max(10).default(5) })
  },
  // Aliases required by brief: findKeyContributors/topic, explainWorkLifecycle, identifyBlockers
  findKeyContributors: { description: 'Analyzes review/author edges for topic/label', parameters: z.object({ topic: z.string(), label: z.string().optional() }) },
  explainWorkLifecycle: { description: 'Traces Issue -> PR -> Reviewer -> Merge', parameters: z.object({ issueNumber: z.number() }) },
  identifyBlockers: { description: 'Finds open dependencies & unapproved reviews', parameters: z.object({ issueNumber: z.number() }) },
};

NestJS Handler — ee/ai-agent/src/graph-rag.service.ts:

@Injectable()
export class GraphRagService {
  async getNeighborhood(params: { repoOwner:string, repoName:string, entityType:'issue'|'pr'|'user', identifier:string, depth?:number }) { /* … see §101.1 … */ }
  async getWorkLifecycle(params: { repoOwner:string, repoName:string, issueOrPrNumber:number }) {
    const rows = await this.prisma.$queryRaw`SELECT ... WHERE (src.entity_number = ${n} OR tgt.entity_number = ${n}) ORDER BY e.created_at`;
    return { workNumber: params.issueOrPrNumber, lifecycleEvents: rows.map(r => ({ event: r.relationship_type, confidence: r.confidence, from: { id: r.src_id, type: r.src_type }, to: { id: r.tgt_id, type: r.tgt_type }, timestamp: r.created_at })) };
  }
}

Next.js Streaming Route — ee/ai-agent/src/route.ts:

import { streamText, tool } from 'ai'; import { openai } from '@ai-sdk/openai';
export async function POST(req: Request, graphRagService: GraphRagService) {
  const { messages, repoContext } = await req.json();
  const result = streamText({
    model: openai('gpt-5-mini'),
    system: `You are the RepoGraph AI Agent for ${repoContext.owner}/${repoContext.name}. Use tools to query the context graph.`,
    messages,
    tools: {
      getNeighborhood: tool({ description: RepoGraphTools.getNeighborhood.description, parameters: RepoGraphTools.getNeighborhood.parameters, execute: async (a) => graphRagService.getNeighborhood(a) }),
      getWorkLifecycle: tool({ description: RepoGraphTools.getWorkLifecycle.description, parameters: RepoGraphTools.getWorkLifecycle.parameters, execute: async (a) => graphRagService.getWorkLifecycle(a) })
    },
    maxSteps: 3
  });
  return result.toDataStreamResponse();
}

101.3 Token Quota & Metering

Tenant Model Quota
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 ee/billing-stripe/token-usage (Redis tokens:{workspaceId}:{month} + Postgres billing_ledger)

Metering middleware ee/ai-agent/src/token.guard.ts checks x-workspace-id + remaining before streamText, returns 429 { code: QUOTA_EXCEEDED } when exhausted, offers BYOK fallback.


102. Commercial Open Source & Open Core Architecture

Model: Open Core monorepo with hard directory isolation.

repograph/
├── apps/
│   ├── web/  # Next.js App Router (OSS UI) — AGPLv3
│   └── api/  # NestJS API Engine (OSS Backend) — AGPLv3
├── packages/
│   ├── graph-model/        # Canonical TS types — AGPLv3
│   └── layout-worker/      # ELK.js calculator — AGPLv3
└── 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

102.1 Licensing Strategy

Scope License Rationale
Core (/apps, /packages) AGPLv3 or FSL (Functional Source License, Busl-1.1 style) Prevents cloud providers/competitors from selling proprietary hosted forks without contributing back. Network copyleft triggers on hosted use.
Enterprise (/ee) Commercial License (proprietary) SaaS-specific: Stripe, GitHub App private sync, multi-tenancy, AI. Not available under AGPL/FSL.

Dual-license note: Contributors sign CLA (see §102.3) granting RepoGraph Inc. relicensing rights for commercial distribution.

102.2 Brand & Trademark Protection

  • Source code is open (AGPLv3/FSL), brand marks are proprietary: name RepoGraph, logos, wordmarks, repograph.app domain assets.
  • Trademark Terms: Hosted forks must rebrand (“Powered by RepoGraph” allowed, “RepoGraph Cloud” not). Automated check in CONTRIBUTING.md + TRADEMARK.md.
  • Enforcement via LICENSE header + ee/ boundary + GitHub CODEOWNERS for /ee.

102.3 Contributor License Agreement (CLA)

  • All community PRs require CLA via cla-assistant GitHub Action (.github/workflows/cla.yml).
  • CLA text: CLA.md — grants patent + copyright license to RepoGraph Inc., ensures FSL/Commercial relicensing, does not transfer ownership.
  • Bot comments CLA not signed and blocks merge until /cla sign.

103. Security, Multi-Tenancy & Private Repositories (Updated — Tenant Isolation)

Zero-friction GitHub App flow (no manual PATs in SaaS):

  1. User clicks Connect GitHubGET /ee/github-app-sync/installhttps://github.com/apps/repograph/installations/new (select org/repos, read-only: contents:read, metadata:read, issues:read, pull_requests:read).
  2. Callback GET /ee/github-app-sync/callback?installation_id → backend stores installation { id, accountLogin }, mints ephemeral token via POST /app/installations/{id}/access_tokens (60m TTL), caches github:installation:{id}:token in Redis (50m), never stores long-lived secret.
  3. Frontend stores only installationId + workspaceId, sends X-Installation-Id + X-Workspace-Id on private graph requests.

Ephemeral Credentials:

  • Zero long-lived repo secrets. ee/github-app-sync/src/token.service.ts getInstallationToken(installationId) → JWT (10m) → GitHub → ghs_... (60m) → Redis 50m. Concurrent callers use SET NX lock.
  • GITHUB_APP_PRIVATE_KEY from ee KMS/Secrets Manager, never repo.

Tenant Isolation:

  • PostgreSQL RLS or compound indices: All private repository_nodes/repository_edges carry workspace_id + installation_id (not just repo_name). Queries must include WHERE workspace_id = $1 AND installation_id = $2 (or RLS policy USING (workspace_id = current_setting('app.workspace_id'))). Compound index (workspace_id, installation_id, repo_name) prevents cross-tenant scan. Add workspace_id to repository_nodes/repository_edges (nullable for public, NOT NULL for private).
  • Enforcement: ee/multi-tenancy/src/tenant.guard.ts extracts x-workspace-id + x-installation-id from headers, sets SET LOCAL app.workspace_id, and rejects if workspaceId mismatches installation.accountLogin.
  • Test: SELECT * FROM repository_nodes WHERE workspace_id != $1 must return 0 for any tenant.

Cache Partitioning (Encrypted, Namespaced):

Scope Redis Key TTL Notes
Public graph:public:{owner}:{repo}:{nodeId} (old graph:{owner}:{repo}:... aliased) 5m active / 7d closed Shared, no workspace
Private graph:private:{workspaceId}:{installationId}:{nodeId} Same split, plus 60m token TTL Encrypted via ee/multi-tenancy/src/crypto.service.ts (AES-256-GCM with REDIS_ENCRYPTION_KEY) when ENCRYPT_PRIVATE_CACHE=true

Implementation: CacheService.graphKey now branches on isPrivate: isPrivate ? graph:private:{workspaceId}:{installationId}:{focus}:{depth} : graph:public:{owner}:{repo}:{focus}:{depth}. Legacy graph: keys remain as alias with private:false for backward compat until cutover (USE_PARTITIONED_CACHE=true).


104. Pricing & Tiering Matrix

Feature Free (Community / OSS) Pro (Developer / Individual) Team / Enterprise
License AGPLv3 self-hosted AGPLv3 + Commercial /ee (hosted) Commercial + SLA
Public URL exploration ✅ Unlimited (3/day/IP soft-gate on hosted) ✅ Unlimited ✅ Unlimited
JIT 1-hop / 2-hop engine
React Flow canvas + ELK worker
AI Chat Assistant (Graph RAG) ❌ (prompt to upgrade) 100k tokens/mo (managed) or BYOK 500k tokens/mo + BYOK + workspace pool
GitHub App private repo sync Self-host only (manual PAT) ✅ 1 workspace, 5 private repos (ephemeral 60m tokens) ✅ Unlimited workspaces, RBAC, RLS
Multi-tenant RBAC ee/multi-tenancy (Owner/Admin/Member, workspace_id RLS)
Stripe billing & quotas ✅ Checkout/portal/webhooks ee/billing-stripe ✅ + invoicing, SSO
Encrypted Redis tenant caches ✅ (private namespaces)
Support Community (GitHub Issues) Email (48h) Slack + SLA (24h), DPA

Billing Implementation: ee/billing-stripe (Checkout POST /ee/billing/checkout, Portal POST /ee/billing/portal, Webhooks POST /ee/billing/webhookworkspace.billingStatus, Redis tokens:{workspaceId}:{month} metering, 429 QUOTA_EXCEEDED).


105. Final Validation (Updated)

Validate GET /repositories/:id/graph?focus=...&depth=1 p95 <800ms and AI getNeighborhood p95 <400ms (Prisma repository_edges 1-hop <10ms + LLM streaming <300ms) on goharbor/harbor.