{
  "slug": "How-I-Made-a-Chatbot-With-My-Knowledge--Turning-My-Technical-Articles-into-a-GraphRAG-Assistant-d3d95c3c8e52",
  "title": "How I Made a Chatbot With My Knowledge: Turning My Technical Articles into a GraphRAG Assistant",
  "subtitle": "As software engineers, our personal blogs and notes often end up as chronological archives — hundreds of Markdown files, JSON payloads, and code repositories scattered across years of work. Traditional vector search (standard Retrieval-Augmented Generation, or RAG) helps find keywords and paragraphs, but it fails when a query requires understanding how disparate technical decisions link together across an entire ecosystem.",
  "excerpt": "As software engineers, our personal blogs and notes often end up as chronological archives — hundreds of Markdown files, JSON payloads, and code repositories scattered across years of work. Traditional vector search…",
  "date": "2026-08-21",
  "tags": [
    "AI",
    "RAG",
    "Knowledge Graphs",
    "GraphRAG",
    "Go"
  ],
  "readingTime": "9 min",
  "url": "https://towardsdev.com/how-i-made-a-chatbot-with-my-knowledge-turning-my-technical-articles-into-a-graphrag-assistant-d3d95c3c8e52",
  "hero": "https://cdn-images-1.medium.com/max/1024/1*z8EbXWNNorfLBg4jqeI-0g.png",
  "content": [
    {
      "type": "paragraph",
      "html": "As software engineers, our personal blogs and notes often end up as chronological archives — hundreds of Markdown files, JSON payloads, and code repositories scattered across years of work. Traditional vector search (standard Retrieval-Augmented Generation, or RAG) helps find keywords and paragraphs, but it fails when a query requires understanding how disparate technical decisions link together across an entire ecosystem."
    },
    {
      "type": "paragraph",
      "html": "To solve this, I transformed my archive into an interconnected <strong>Knowledge Graph</strong> and built a <strong>GraphRAG architecture assistant</strong>. Here is the step-by-step technical journey of how I structured the data, extracted entities and hyperedges, computed semantic similarities, and wired everything to an LLM."
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:700/1*z8EbXWNNorfLBg4jqeI-0g.png",
      "alt": "How I Made a Chatbot With My Knowledge: Turning My Technical Articles into a GraphRAG Assistant",
      "caption": ""
    },
    {
      "type": "heading",
      "level": 2,
      "text": "1. The Challenge: Transforming Unstructured JSON into Documents"
    },
    {
      "type": "paragraph",
      "html": "My blog archive (<code>src/data</code>) contained raw JSON files spanning article dumps, GitHub project repositories, video records (YouTube/Aparat), and author metadata:"
    },
    {
      "type": "paragraph",
      "html": "src/data/"
    },
    {
      "type": "paragraph",
      "html": "├── aparat-videos.json"
    },
    {
      "type": "paragraph",
      "html": "├── github-projects.json"
    },
    {
      "type": "paragraph",
      "html": "├── posts/"
    },
    {
      "type": "paragraph",
      "html": "│ ├── Real-Time-Data-Ingestion-Kafka-to-ClickHouse.json"
    },
    {
      "type": "paragraph",
      "html": "│ └── ..."
    },
    {
      "type": "paragraph",
      "html": "├── profile.json"
    },
    {
      "type": "paragraph",
      "html": "└── youtube-videos.json When feeding raw <code>.json</code> files into AST-based code parsers (like Graphify), the engine classified them as source code rather than semantic documents, resulting in zero nodes."
    },
    {
      "type": "paragraph",
      "html": "To resolve this, I built an automated transformer script (<code>build_kg_docs.py</code>) to convert all JSON records into clean, structured Markdown documents with explicit headers and frontmatter:"
    },
    {
      "type": "paragraph",
      "html": "def write_md(filename, title, meta, body):"
    },
    {
      "type": "paragraph",
      "html": "filepath = os.path.join(OUT_DIR, f&quot;{filename}.md&quot;)"
    },
    {
      "type": "paragraph",
      "html": "content = f&quot;# {title}\\n\\n&quot;"
    },
    {
      "type": "paragraph",
      "html": "for k, v in meta.items():"
    },
    {
      "type": "paragraph",
      "html": "if v:"
    },
    {
      "type": "paragraph",
      "html": "content += f&quot;<strong>{k}:</strong> {v}\\n&quot;"
    },
    {
      "type": "paragraph",
      "html": "content += f&quot;\\n{body}\\n&quot;"
    },
    {
      "type": "paragraph",
      "html": "with open(filepath, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:"
    },
    {
      "type": "paragraph",
      "html": "f.write(content) This formatted 525 documents into an <code>/md</code> staging directory, ready for semantic extraction."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "2. Extracting the Knowledge Graph"
    },
    {
      "type": "paragraph",
      "html": "Using <code>graphify</code> routed through an OpenAI-compatible proxy (<code>api.avalai.ir/v1</code>) using <code>gpt-5.6-luna</code>, I extracted semantic entity-relation-entity triples and multi-component system hyperedges:"
    },
    {
      "type": "paragraph",
      "html": "env $(cat .env | xargs) graphify extract ./md --backend openai --model gpt-5.6-luna"
    },
    {
      "type": "paragraph",
      "html": "env $(cat .env | xargs) graphify cluster-only ./md --backend openai --model gpt-5.6-luna"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Key Graph Extraction Metrics"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Total Entities &amp; Nodes</strong>: 426 nodes",
        "<strong>Total Directed &amp; Semantic Edges</strong>: 353 relationships (93% extracted, 7% inferred)",
        "<strong>Topic Communities Detected</strong>: 111 distinct architectural clusters grouped via the Leiden algorithm"
      ]
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:531/1*EB0lQfldvhMf8UpjVtsljA.png",
      "alt": "How I Made a Chatbot With My Knowledge: Turning My Technical Articles into a GraphRAG Assistant",
      "caption": ""
    },
    {
      "type": "paragraph",
      "html": "The resulting <code>graph.json</code> uncovered the core pillars of my technical writing:"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>ClickHouse &amp; Streaming Ingestion</strong> (9 direct connections)",
        "<strong>Retrieval-Augmented Generation &amp; Vector Stores</strong> (12 connections)",
        "<strong>Fine-Grained Authorization (Keycloak &amp; OpenFGA)</strong> (10 connections)",
        "<strong>Modular Go Architecture (Clean Architecture, Uber FX, SQLC)</strong> (10 connections)"
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "3. Eliminating “Hub-Node Distortion” in Article Similarities"
    },
    {
      "type": "paragraph",
      "html": "To make this graph usable in my web frontend (a TanStack Router blog), I needed to compute the similarity between articles."
    },
    {
      "type": "paragraph",
      "html": "Initially, applying standard Jaccard similarity across shared neighbors resulted in false positives: unrelated posts that shared only a single broad concept like <code>#Machine Learning</code> received a similarity score of 1.0."
    },
    {
      "type": "paragraph",
      "html": "To fix this, I implemented an <strong>Adamic-Adar / Inverse-Degree Weighted Similarity</strong> algorithm in Python:"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:548/1*DECRUBpriD6OIzcNxux8Cg.png",
      "alt": "How I Made a Chatbot With My Knowledge: Turning My Technical Articles into a GraphRAG Assistant",
      "caption": ""
    },
    {
      "type": "paragraph",
      "html": "If two articles shared a rare technical concept (such as <code>ClickHouse Kafka Engine</code> with 2 edges), it provided a high score; if they shared an umbrella tag with 24 edges, the weight approached zero. This generated clean, high-confidence mappings for my site&#39;s <code>related_posts.json</code>."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "4. Building the GraphRAG Reasoning Engine"
    },
    {
      "type": "paragraph",
      "html": "Standard RAG searches for direct text similarity, but real software architecture questions are <strong>multi-hop</strong>. For example:"
    },
    {
      "type": "quote",
      "html": "“How does the streaming Kafka ingestion pipeline handle deadlocks and schemas when inserting into ClickHouse?”"
    },
    {
      "type": "paragraph",
      "html": "A standard vector database might return a snippet about Kafka or a snippet about ClickHouse. GraphRAG, however, traverses the structural path:"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:700/1*asemKbu47bneSNaX08Ix0Q.png",
      "alt": "How I Made a Chatbot With My Knowledge: Turning My Technical Articles into a GraphRAG Assistant",
      "caption": ""
    },
    {
      "type": "heading",
      "level": 2,
      "text": "The Python Implementation"
    },
    {
      "type": "paragraph",
      "html": "I built <code>graph_rag.py</code> to ingest <code>graph.json</code>, perform Breadth-First Search (BFS) graph expansion from seed entities, and provide the extracted subgraph to the LLM:"
    },
    {
      "type": "code",
      "lang": "python",
      "code": "import os, json, requests\nfrom collections import defaultdict\nfrom typing import List, Dict, Any\nGRAPH_JSON_PATH = \"./md/graphify-out/graph.json\"\nOPENAI_BASE_URL = os.getenv(\"OPENAI_BASE_URL\", \"https://api.avalai.ir/v1\")\nOPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\nMODEL_NAME = os.getenv(\"OPENAI_MODEL\", \"gpt-5.6-luna\")\nclass GraphRAGRetriever:\n def  __init__ (self, graph_path: str):\n with open(graph_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\nself.nodes = {n[\"id\"]: n for n in data.get(\"nodes\", [])}\n self.adjacency = defaultdict(set)\n self.hyperedges = data.get(\"graph\", {}).get(\"hyperedges\", [])\nfor link in data.get(\"links\", []):\n self.adjacency[link[\"source\"]].add(link[\"target\"])\n self.adjacency[link[\"target\"]].add(link[\"source\"])\n def traverse(self, seeds: List[str], depth: int = 2, max_nodes: int = 15):\n visited = set(seeds)\n queue = [(s, 0) for s in seeds]\nwhile queue and len(visited) < max_nodes:\n curr, d = queue.pop(0)\n if d >= depth:\n continue\n for neighbor in self.adjacency.get(curr, []):\n if neighbor not in visited:\n visited.add(neighbor)\n queue.append((neighbor, d + 1))\nreturn [self.nodes[nid] for nid in visited if nid in self.nodes]\n def query_graph_rag(user_query: str):\n retriever = GraphRAGRetriever(GRAPH_JSON_PATH)\n seeds = [nid for nid, n in retriever.nodes.items() if n.get(\"label\", \"\").lower() in user_query.lower()]\n subgraph_nodes = retriever.traverse(seeds or [\"concept_clickhouse\", \"concept_kafka\"])\ncontext = \"\\n\".join([f\"- {n['label']} ({n.get('file_type')}): {n.get('rationale', '')}\" for n in subgraph_nodes])\nprompt = f\"\"\"Use the following knowledge graph context to answer the architectural question:\nContext:\n{context}\nQuestion: {user_query}\"\"\"\n res = requests.post(\n f\"{OPENAI_BASE_URL.rstrip('/')}/chat/completions\",\n headers={\"Authorization\": f\"Bearer {OPENAI_API_KEY}\"},\n json={\"model\": MODEL_NAME, \"messages\": [{\"role\": \"user\", \"content\": prompt}]}\n ).json()\nreturn res[\"choices\"][0][\"message\"][\"content\"]"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "5. Integrating with the Frontend (TanStack Router)"
    },
    {
      "type": "paragraph",
      "html": "In the frontend router (<code>/blog/$slug</code>), I connected the graph mapping to a normalized slug resolver to guarantee resilient fuzzy matching across hash suffixes (<code>-530ff573762f</code>) and index identifiers (<code>--17</code>):"
    },
    {
      "type": "paragraph",
      "html": "function normalizeSlug(str: string): string {"
    },
    {
      "type": "paragraph",
      "html": "return str"
    },
    {
      "type": "paragraph",
      "html": ".toLowerCase()"
    },
    {
      "type": "paragraph",
      "html": ".replace(/^(post<em>index</em>|post<em>|project</em>)/, &#39;&#39;)"
    },
    {
      "type": "paragraph",
      "html": ".replace(/-[a-f0-9]{8,16}$/, &#39;&#39;)"
    },
    {
      "type": "paragraph",
      "html": ".replace(/-\\d+$/, &#39;&#39;)"
    },
    {
      "type": "paragraph",
      "html": ".replace(/_/g, &#39;-&#39;)"
    },
    {
      "type": "paragraph",
      "html": ".trim();"
    },
    {
      "type": "paragraph",
      "html": "}"
    },
    {
      "type": "paragraph",
      "html": "function relatedPostSlugs(slug: string, articles: IndexEntry[]): IndexEntry[] {"
    },
    {
      "type": "paragraph",
      "html": "const relationMap = relatedPosts as Record&lt;string, string[]&gt;;"
    },
    {
      "type": "paragraph",
      "html": "const cleanCurrentSlug = normalizeSlug(slug);"
    },
    {
      "type": "paragraph",
      "html": "const matchedKey = Object.keys(relationMap).find(key =&gt; normalizeSlug(key) === cleanCurrentSlug);"
    },
    {
      "type": "paragraph",
      "html": "if (!matchedKey) return [];"
    },
    {
      "type": "paragraph",
      "html": "return relationMap[matchedKey]"
    },
    {
      "type": "paragraph",
      "html": ".map(relSlug =&gt; articles.find(a =&gt; normalizeSlug(a.slug) === normalizeSlug(relSlug)))"
    },
    {
      "type": "paragraph",
      "html": ".filter((a): a is IndexEntry =&gt; Boolean(a));"
    },
    {
      "type": "paragraph",
      "html": "}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "What This Enables"
    },
    {
      "type": "paragraph",
      "html": "By converting flat articles into an interconnected Knowledge Graph and GraphRAG engine:"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>Interactive Navigation</strong>: Readers see genuinely related posts based on shared architectural topology rather than simple keyword collisions.",
        "<strong>Context-Aware Chatbot</strong>: An AI assistant that understands the <em>relationships</em> between my Go workers, Kafka pipelines, and ClickHouse databases.",
        "<strong>Auditing Knowledge Gaps</strong>: The graph immediately surfaced 256 isolated concepts and thin communities, giving me a clear roadmap of which deep-dive articles to write next."
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Example:"
    },
    {
      "type": "paragraph",
      "html": "<strong>python3 graph_rag.py</strong>"
    },
    {
      "type": "paragraph",
      "html": "=== GraphRAG Architecture Assistant (Type ‘exit’ or Ctrl+C to quit) ==="
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Ask a question: How can we design a software architecture capable of handling 100,000 requests per second (high TPS)? Please explain the key architectural components and strategies involved."
    },
    {
      "type": "paragraph",
      "html": "— — Traversing Graph &amp; Querying deepseek-v4-flash — -"
    },
    {
      "type": "paragraph",
      "html": "— — Model Response — -"
    },
    {
      "type": "paragraph",
      "html": "To architect for <strong>100,000 requests per second (100k RPS)</strong>, you cannot rely on a monolithic, synchronous request/response cycle. The bottleneck will shift from application logic to data persistence, distributed coordination, and observability. Based on the architectural patterns and documented pipelines in the knowledge graph, you need a <strong>decoupled, event-driven, layered system</strong> that aggressively offloads work from the hot path."
    },
    {
      "type": "paragraph",
      "html": "Here is the comprehensive blueprint, mapped to the specific documented components:"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "1. Edge Layer (Request Offloading & Routing)"
    },
    {
      "type": "paragraph",
      "html": "At 100k RPS, you must intercept and filter traffic before it hits your origin servers."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Cloudflare Workers for Edge Request Handling.</strong> Deploy serverless Workers at the edge to handle CORS, request routing, proxying, and lightweight authentication (e.g., API key validation). This absorbs the bulk of invalid or pre-flight requests, drastically reducing the load on your core API services."
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Strategy: Strategy Design Pattern.</strong> Use this in the edge logic to isolate routing/sorting algorithms (e.g., geographic routing vs. tenant-based routing) so you can swap traffic-shaping logic without redeploying the entire origin stack."
      ]
    },
    {
      "type": "heading",
      "level": 3,
      "text": "2. Core API Layer (Stateless, Scalable Services)"
    },
    {
      "type": "paragraph",
      "html": "Your application tier must be stateless to scale horizontally with a load balancer."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Layered Clean Architecture (Go Web API).</strong> Structure the core services strictly into Model, Repository, Service, Controller, and DTO layers. This prevents code duplication and reduces coupling, allowing each microservice to be independently scaled and deployed. Repositories are isolated, so the database driver can be swapped without impacting the service logic."
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Pattern: CQRS Microservices Architecture.</strong> Split the read and write paths. For 100k RPS, reads vastly outnumber writes. CQRS allows you to scale the read models independently (using caching) and the write models (using queues), preventing database contention."
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Pattern: Domain-Driven Design (DDD) Dependency Inversion.</strong> Apply DDD to enforce dependency inversion, particularly for billing and critical transactions (e.g., Stripe). Keep the core domain logic pure and independent of infrastructure, ensuring high throughput and testability without I/O blocking."
      ]
    },
    {
      "type": "heading",
      "level": 3,
      "text": "3. Data Layer (Shortening the Hot Path)"
    },
    {
      "type": "paragraph",
      "html": "Direct synchronous database hits will cap your throughput well below 100k RPS. You must use a multi-tier data access strategy."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Pattern: High-Scale Data Processing (Redis + Async DB).</strong> Implement the <strong>High-Traffic Click Application Design</strong>. All incoming requests write to <strong>Redis</strong> first (in-memory, sub-millisecond latency) and read from Redis for immediate consistency. Do <strong>not</strong> hit the primary database (e.g., PostgreSQL) on the request thread."
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Pattern: Database-per-service Isolation.</strong> Strictly enforce database-per-service. A shared database is a distributed lock and a single point of failure — it creates coupling, resource contention, and ambiguous ownership, which will cause cascading failures at 100k TPS. The graph explicitly recommends isolation to prevent this."
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Flat Columnar KPI Architecture.</strong> For any analytics or KPI aggregation (e.g., dashboard reads), use <strong>ClickHouse</strong> with flat columns instead of JSON blobs. This allows vectorized aggregation and columnar storage to read only specific counters, which scales far better than parsing nested data on the fly."
      ]
    },
    {
      "type": "heading",
      "level": 3,
      "text": "4. Asynchronous Processing Pipeline (The Heartbeat)"
    },
    {
      "type": "paragraph",
      "html": "To handle the write volume and offload heavy processing, use a Fire-and-Forget + Event-Driven architecture."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Kafka ClickHouse Ingestion Pipeline (High-Scale Framework).</strong>"
      ]
    },
    {
      "type": "paragraph",
      "html": "— <strong>Writes Path</strong>: The API layer pushes the event to <strong>Kafka</strong> (or a similar log-based queue). Kafka acts as a massive buffer that absorbs write spikes."
    },
    {
      "type": "paragraph",
      "html": "— <strong>Reads Path</strong>: Redis remains the source of truth for immediate reads; Kafka replicates to ClickHouse for historical queries."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Pattern: Poller–Dispatcher–Worker Architecture.</strong> Once events hit Kafka, use a Poller to batch-pull messages, a Dispatcher to route them to bounded worker queues, and separate Worker pools to process side-effects (e.g., email, report generation, database aggregation). This isolates error domains — if one worker fails, it only backs up its bounded queue, preventing cascading failure across the system. Support graceful shutdown and independent scaling for these workers."
      ]
    },
    {
      "type": "heading",
      "level": 3,
      "text": "5. Security & Authorization (Scale-Aware)"
    },
    {
      "type": "paragraph",
      "html": "AuthN/AuthZ must not be a bottleneck. Implement OAuth2/OIDC + fine-grained permissions."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Keycloak and OpenFGA Authorization Architecture.</strong> Use <strong>Keycloak</strong> for centralized OIDC token issuance (this is a heavy operation, do it rarely — use token caching). Use <strong>OpenFGA</strong> (Google Zanzibar pattern) for highly scalable, distributed, fine-grained authorization checks. OpenFGA is designed for low-latency, high-fanout relationship checks, which is critical at 100k RPS."
      ]
    },
    {
      "type": "heading",
      "level": 3,
      "text": "6. Observability & CI/CD (The Control Plane)"
    },
    {
      "type": "paragraph",
      "html": "At this scale, “unknown” is the real killer. You need complete visibility and rapid deployment."
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Go OpenTelemetry Observability Guidance.</strong> Instrument <em>every</em> request using OpenTelemetry tracing (spans). Integrate this with <strong>Sentry</strong> to map traces to transactions and spans for automatic error tracking and latency bucketing. This is non-negotiable for diagnosing downstream bottlenecks in Redis, Kafka, or DBs."
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Component: Architectural Optimization of Shared Sandbox Deployments (GitLab CI/CD).</strong> Implement isolated, ephemeral sandbox environments for performance testing and parallel deployment. Use GitLab CI/CD with matrix builds to test the 100k RPS load against separate stacks, ensuring zero regression before rolling out to the main fleet."
      ]
    },
    {
      "type": "paragraph",
      "html": "— -"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Summary Execution Flow at 100k RPS:"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>Request</strong> hits <strong>Cloudflare Worker</strong> (edge). CORS/Auth filtered."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>Controller</strong> (Layered Clean Arch) parses the request and validates shape."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "Service uses <strong>Strategy Pattern</strong> to select the handler."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>Redis</strong> is hit immediately (GET/SET). If cache hit, respond immediately (90%+ of requests)."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "If write: Event pushed to <strong>Kafka</strong>. Redis is updated optimistically. Response returns 200 to the client in &lt; 5ms."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>Poller-Dispatcher-Worker</strong> consumes Kafka, updates the primary <strong>Database-per-service</strong> (Postgres) asynchronously."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>ClickHouse</strong> ingests flattened logs via CDC/Kafka for analytics."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>OpenFGA</strong> validates permissions only for non-cached resources."
      ]
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "<strong>OpenTelemetry</strong> tags the trace, and Sentry tracks the event."
      ]
    },
    {
      "type": "paragraph",
      "html": "This architecture is successful because it <strong>eliminates synchronous I/O</strong> from the request path. Redis absorbs the heat, Kafka buffers the write burst, and ClickHouse accelerates analytics. Every component scales independently, and the graph patterns dictate strict data possession and asynchronous data flows — which is precisely how you breach the 100k RPS threshold."
    }
  ]
}