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.
- 2D & 3D force-directed graph — toggle between
react-force-graph-2dandreact-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=#ff9500and 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-frameonBeforeRenderopacity curve), so you can always read the cluster you're close to. The selected node's label is forced fully opaque anddepthTest-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), andQ/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
getImageDatarandomization that breaks force-graph's default shadow-canvas hit detection.
cp .env.example .env
# edit .env: set VITE_CORTEX_API_BASE_URL + VITE_CORTEX_API_KEY
npm install
npm run devOpen http://localhost:5173.
| Variable | Purpose |
|---|---|
VITE_CORTEX_API_BASE_URL | Base URL of your Cortex backend, e.g. https://cortex.example.com (no trailing slash) |
VITE_CORTEX_API_KEY | A read-only API key from your Cortex admin UI |
VITE_ACCENT_COLOR | Optional 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).
| Action | 2D | 3D |
|---|---|---|
| Select node | Click | Click |
| Drag a node | Click + drag | Click + drag |
| Pan | Drag background | Right-drag |
| Zoom | Scroll | Scroll |
| Orbit | — | Left-drag |
| Engage flight mode | — | Click canvas (focuses it) |
| Fly forward / back | — | W / S |
| Strafe left / right | — | A / D |
| Rise / descend | — | R / F |
| Yaw left / right | — | Q / E |
| Flight cheatsheet | — | Click the rocket button (bottom-left) |
| Close panel | ESC / click empty canvas | ESC / click empty canvas |
| Dismiss search results | ESC / click × / click outside | same |
| Clear search query | ESC again after dropdown closed | same |
npm run build # → static bundle in dist/
npm run preview # → http://localhost:4173, serves dist/ as-isThe 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.
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.
- Push this repo to your Git host.
- In Coolify: Projects → your project → + New Resource → Application → Public Repository (or Private Repository with GitHub App), point at the repo, branch
main. - Build Pack: Nixpacks.
- General tab:
- Domains:
https://explorer.example.com(full URL with scheme; Traefik auto-issues a Let's Encrypt cert) - Ports Exposes:
80
- Domains:
- Build tab:
- Is it a static site? ✅ (required — otherwise Coolify tries to
npm startand 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.htmlfor unknown paths. - Install Command:
npm ci(or leave blank — Nixpacks autodetects) - Build Command:
npm run build(or leave blank) - Publish Directory:
dist
- Is it a static site? ✅ (required — otherwise Coolify tries to
- 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 URLVITE_CORTEX_API_KEY= read-only API keyVITE_ACCENT_COLOR= optional CSS color
- 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).
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.
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.
<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.
All endpoints are X-API-Key-authenticated and read-only:
| Endpoint | Used for |
|---|---|
GET /api/graph/visualization | Initial graph render |
GET /api/graph/search | Entity search dropdown |
GET /api/graph/entity/{name} | Details panel |
GET /api/graph/entity/{name}/relationships | Click-to-expand 1-hop fetch |
POST /api/graph/subgraph | Multi-select subgraph + bridge subgraph for expansion |
See the Cortex API docs for full response shapes.
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.
- Blank screen / "Cortex Explorer isn't configured" —
VITE_*env vars weren't inlined at build time. Locally: check.envhasVITE_CORTEX_API_BASE_URL+VITE_CORTEX_API_KEYand restartnpm 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
readpermission. - 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 startand 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.
Vite 6 · React 19 · TypeScript 5 · Tailwind CSS 3 · react-force-graph-2d · react-force-graph-3d · three · three-spritetext · lucide-react
npm run dev— Vite dev server on :5173npm run build— Typecheck + static build todist/npm run preview— Preview the production build locallynpm run lint— TypeScript type-check only
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