Skip to content

Repository files navigation

Cortex Explorer

Standalone, bleeding-edge visualizer for Cortex knowledge graphs.

Drop it in front of any running Cortex backend with a read-only API key and you get a full-screen, iframe-friendly graph explorer with 2D / 3D toggle, entity search, click-to-drill, and click-to-expand neighborhood traversal — all as a single static bundle.

Features

  • 2D & 3D force-directed graph — toggle between react-force-graph-2d and react-force-graph-3d, built on vasturiano/force-graph and vasturiano/3d-force-graph.
  • Accent-derived palette — the whole graph rebrands off one env var. Set CORTEX_ACCENT_COLOR=#ff9500 and every entity type becomes a hue-family variation of that accent; swap to cyan and the palette re-generates. Selected nodes + their incident edges glow in the exact accent.
  • Persistent 3D labels with depth fade — every node's label stays visible in 3D and fades smoothly with camera distance (via three-spritetext + a per-frame onBeforeRender opacity curve), so you can always read the cluster you're close to. The selected node's label is forced fully opaque and depthTest-skipped so it never hides behind nearby spheres.
  • Spaceship-style 3D flight — click the 3D canvas to engage, then fly with WASD (translate), R/F (rise/descend), and Q/E (yaw). Every axis is rebuilt each frame from the camera's current quaternion, so mouse-drag rotations are folded in before the keys fire — movement always stays relative to where the ship is pointing. A bottom-left rocket icon toggles a cheatsheet panel.
  • Entity search — debounced search with an ESC-dismissable dropdown, multi-select to pull up a focused subgraph.
  • Entity details panel — click a node for description, related entities, relationships, and source documents.
  • Click-to-expand exploration — click any related entity in the panel and the graph fetches its 1-hop neighborhood + the bridge subgraph connecting it to the current selection, merges everything, then pans/zooms onto the new node.
  • Build-time configuration — env vars are inlined by Vite at build; one redeploy to rotate keys, swap the backend URL, or rebrand the palette.
  • Iframe-ready — edge-to-edge layout, no page chrome, no auth UI, no X-Frame-Options.
  • Works in Brave & other fingerprinting-hardened browsers — geometric click detection fallback bypasses the getImageData randomization that breaks force-graph's default shadow-canvas hit detection.

Quick start — local dev

cp .env.example .env
# edit .env: set VITE_CORTEX_API_BASE_URL + VITE_CORTEX_API_KEY
npm install
npm run dev

Open http://localhost:5173.

Environment variables

VariablePurpose
VITE_CORTEX_API_BASE_URLBase URL of your Cortex backend, e.g. https://cortex.example.com (no trailing slash)
VITE_CORTEX_API_KEYA read-only API key from your Cortex admin UI
VITE_ACCENT_COLOROptional accent color (any valid CSS color — hex, oklch(), rgb(), named). Defaults to #ff9500.

All three are build-time: Vite inlines them into the JS bundle during vite build, so they must be present when the build runs (in .env locally, in Coolify's env UI with the Build Variable checkbox ticked in production).

Controls

Action2D3D
Select nodeClickClick
Drag a nodeClick + dragClick + drag
PanDrag backgroundRight-drag
ZoomScrollScroll
OrbitLeft-drag
Engage flight modeClick canvas (focuses it)
Fly forward / backW / S
Strafe left / rightA / D
Rise / descendR / F
Yaw left / rightQ / E
Flight cheatsheetClick the rocket button (bottom-left)
Close panelESC / click empty canvasESC / click empty canvas
Dismiss search resultsESC / click × / click outsidesame
Clear search queryESC again after dropdown closedsame

Production build (local)

npm run build # → static bundle in dist/
npm run preview # → http://localhost:4173, serves dist/ as-is

The bundle is a plain folder of static files. Any host that serves static content will do — Coolify, Netlify, Vercel, S3+CloudFront, a box running python -m http.server dist, whatever.

Deploy to Coolify

This app deploys as a static site via the Nixpacks build pack. Coolify runs npm ci && npm run build, then serves dist/ with its built-in Nginx.

  1. Push this repo to your Git host.
  2. In Coolify: Projects → your project → + New ResourceApplicationPublic Repository (or Private Repository with GitHub App), point at the repo, branch main.
  3. Build Pack: Nixpacks.
  4. General tab:
    • Domains: https://explorer.example.com (full URL with scheme; Traefik auto-issues a Let's Encrypt cert)
    • Ports Exposes: 80
  5. Build tab:
    • Is it a static site? ✅ (required — otherwise Coolify tries to npm start and the deploy fails)
    • Is it an SPA? — leave unchecked for this app (no client-side routing, so legitimate 404s should 404). Check it only if you add client-side routes later; it makes Nginx fall back to /index.html for unknown paths.
    • Install Command: npm ci (or leave blank — Nixpacks autodetects)
    • Build Command: npm run build (or leave blank)
    • Publish Directory: dist
  6. Environment Variables tab — add all three with the Build Variable checkbox ticked. Runtime Variable doesn't matter here (a static site has no runtime to read env from):
    • VITE_CORTEX_API_BASE_URL = your Cortex backend URL
    • VITE_CORTEX_API_KEY = read-only API key
    • VITE_ACCENT_COLOR = optional CSS color
  7. Deploy. First build ≈ 30–60 s, subsequent builds cache npm ci.

Verify: open the domain in a browser, or curl -I https://<your-domain>/ (expect HTTP/2 200, real Let's Encrypt cert, no X-Frame-Options).

Rotating env vars

Env vars are baked into the JS bundle during vite build, so rotation requires a rebuild. Edit the value in Coolify's UI → Redeploy (not Restart). Takes ~30 s.

If the deploy succeeds but the domain shows "no available server" with a TRAEFIK DEFAULT CERT

Traefik didn't pick up the new container's labels. Go to Servers → your server → Proxy and click the Restart Proxy button in the header. Takes ~60 s to re-scan containers and issue a real Let's Encrypt cert.

Embedding in an iframe

<iframesrc="https://explorer.example.com"
style="width:100%;height:100vh;border:0"
title="Cortex Explorer"
></iframe>

The app owns the full viewport; the iframe fills whatever container you drop it in.

API surface consumed

All endpoints are X-API-Key-authenticated and read-only:

EndpointUsed for
GET /api/graph/visualizationInitial graph render
GET /api/graph/searchEntity search dropdown
GET /api/graph/entity/{name}Details panel
GET /api/graph/entity/{name}/relationshipsClick-to-expand 1-hop fetch
POST /api/graph/subgraphMulti-select subgraph + bridge subgraph for expansion

See the Cortex API docs for full response shapes.

Security

The read-only API key is bundled/injected into the browser — use a key that only has read permission. The Cortex backend enforces permission checks server-side (require_read_permission), so even if the key leaks, the blast radius is bounded to data you were already exposing via this explorer.

If you need the key fully server-side, put the container behind your own reverse proxy and inject the X-API-Key header there instead of via config.js.

Troubleshooting

  • Blank screen / "Cortex Explorer isn't configured"VITE_* env vars weren't inlined at build time. Locally: check .env has VITE_CORTEX_API_BASE_URL + VITE_CORTEX_API_KEY and restart npm run dev. On Coolify: open the deployed bundle's JS (curl https://<domain>/assets/index-*.js | grep <your-api-host>) — if your host doesn't appear, the Build Variable checkbox wasn't ticked on those env vars. Fix it and Redeploy (rotating env vars on a static build requires a rebuild, not a restart).
  • CORS errors — the Cortex backend ships with allow_origins=["*"], so this is rare. If you've tightened it, add the explorer's origin (e.g. https://explorer.example.com) to the allow-list.
  • 401 Unauthorized on every request — key is wrong, expired, or lacks read permission.
  • Empty graph with 200 responses — the backend has no entities yet. Ingest documents via the Cortex UI first.
  • Coolify deploys succeed but the domain returns "no available server" / TRAEFIK DEFAULT CERT — Traefik didn't pick up the new container's labels. In Coolify, go to Servers → your server → Proxy and click Restart Proxy. Wait ~60 s for Let's Encrypt to issue the real cert.
  • "Is it a static site?" unchecked — Coolify tries to npm start and the deploy fails fast. Check the box and redeploy.
  • 3D view slow with 5k+ nodes — reduce the Node limit dropdown. Per-frame label opacity work is O(n), so very large graphs will hit frame-rate limits.

Tech stack

Vite 6 · React 19 · TypeScript 5 · Tailwind CSS 3 · react-force-graph-2d · react-force-graph-3d · three · three-spritetext · lucide-react

Scripts

  • npm run dev — Vite dev server on :5173
  • npm run build — Typecheck + static build to dist/
  • npm run preview — Preview the production build locally
  • npm run lint — TypeScript type-check only

Repo layout

cortex-explorer/
├── public/config.js # empty stub; kept so index.html's <script src="/config.js"> never 404s
└── src/
├── config.ts # picks runtime __CORTEX_CONFIG__ first, falls back to import.meta.env.VITE_*
├── lib/
│ ├── palette.ts # accent → 5-shade palette + accentLight/accentDark
│ ├── colors.ts # entity type → palette slot
│ ├── api.ts # typed X-API-Key client
│ └── graphUtils.ts # node radius, merge, dedupe
├── hooks/
│ └── useGraphState.ts # owns base graph + expansions + selection
└── components/
├── Explorer.tsx
├── GraphView.tsx # 2D/3D switcher (3D lazy-loaded)
├── GraphCanvas2D.tsx # geometric-hit-detection fallback for Brave
├── GraphCanvas3D.tsx # depth-fade persistent labels via three-spritetext
├── ControlsBar.tsx
├── EntitySearch.tsx
└── EntityPanel.tsx

About

Official Cortex Knowledge Graph Explorer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages