Skip to documentation

Module references

Module reference / Connect it

AtlasThe semantic layer

Atlas

A typed, curated entity graph turns a source corpus into a shared semantic layer with evidence on every relation.
  • Intake

    Receive normalized source material and sentence-level evidence.

  • Warrant

    Apply authority, lifecycle, ownership, and governed change.

  • Lens

    Feed graph neighborhoods into grounded retrieval.

Atlas is the module that turns names into addressable concepts. It owns the entities, their aliases, the typed directed edges between them, and the mention-level provenance that says which document and which character offsets produced each fact. Everything below is served by pure SQL — no model, no embedding, no reranker.

What it does

Atlas stores a workspace's concepts as typed entities and connects them with typed, directed relations. Duplicates collapse into a single canonical head, and the serve-time edge table (canonical_relations) is a materialized rollup over that head, so a read never has to reason about merges. Every entity carries the aliases it answers to, a mention count, and the documents it was found in; every extracted edge can carry the quote that grounds it. The public surface is a list, a single concept read, a bounded neighborhood expansion, a workspace manifest, and hand-authored writes for entities and relations.

Atlas entity and relation lifecycle

Writes land on entities/relations (by hand, or from ingestion when Intake is present); reads are served from the canonical rollup plus the mention provenance. graph_layout supplies the pagerank on the list endpoint.

Rendering diagram

Downloads

Concepts

  • Atlas flow
  • Modules
  • Atlas

Keywords

  • entities + entity_aliases
  • relations + relation_evidence
  • ingestion extraction
  • optional · Intake
  • GET /entities
  • GET /entities/:id
  • GET /manifest
  • POST /entities · POST /relations
  • the only edge table read at serve time
  • doc · chunk · offsets · surface text
  • GET /entities/:id/neighborhood
  • pagerank of the current layout version
  • canonical_relations
  • graph_layout
  • kb:write
  • mentions
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:31:06.344Z

Source hash: 54a5a21384a9e3c7061f34509e811c1ebb237371365376f8bdd58d306f89c244

Metadata payload hash: 2065d2d6f56d85f68e2e18d83802e8722eb354b796da6322c498b4152f703e13

Canonical appearance

src/app/(docs)/docs/modules/atlas/page.tsx:56 route /docs/modules/atlas

All appearances

  • canonicalsrc/app/(docs)/docs/modules/atlas/page.tsx:56 route /docs/modules/atlas

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: atlas-entity-and-relation-lifecycle-54a5a213.json

Use it standalone

Atlas is self-contained. The only things it needs are a workspace and an API key with the right scopes.

  1. Give the key the graph scopes

    Reads (GET /entities, GET /entities/:id, the neighborhood, the manifest) require kb:read; the legacy runs:read grant is still accepted on those four. Writes (POST/PATCH/DELETE on entities and relations) require kb:write. Scopes are checked by exact membership — kb:write does not imply kb:read, so a key that both reads and edits the graph needs both. Delegated user keys (eli_uk_) additionally have their owner's live capabilities re-checked: graph:read for the entity reads, graph:write for the writes, and workspace:read for the manifest.
  2. Import one module

    Headless: createGraphClient from @eli-ai/client/graph. React: EliKnowledgeGraph from @eli-ai/react/graph. Types-only: @eli-ai/contracts/graph. None of the three pulls in another capability module.
  3. Seed the graph by hand, or let ingestion fill it

    POST /api/v1/entities and POST /api/v1/relations are enough to build a complete graph with no documents at all. If documents are present, extraction writes into the same tables and the reads are unchanged.
server-side, Atlas onlyts
import { createEliTransport } from "@eli-ai/client/core";
import { createGraphClient } from "@eli-ai/client/graph";

const graph = createGraphClient(
  createEliTransport({
    baseUrl: "https://eli.ai",
    apiKey: () => process.env.ELI_API_KEY!,
    workspaceId: process.env.ELI_WORKSPACE_ID,
  }),
);

// Hand-author two concepts and the edge between them.
const platform = await graph.createEntity({
  name: "Order Platform",
  type: "system",
  aliases: ["order-platform", "OP"],
});
const owner = await graph.createEntity({ name: "Ada Byron", type: "person" });

await graph.createRelation({
  srcEntityId: owner.id,
  dstEntityId: platform.id,
  type: "owns",
  evidence: "Ada is listed as the service owner in the on-call rota.",
});

// Read it back.
const page = await graph.listEntities({ limit: 25, type: "system" });
const around = await graph.neighborhood(platform.id, { depth: 2, perHopCap: 10 });
console.log(page.total, around.edges.length, around.truncated);

What Atlas does NOT require

No AI provider or model slot — every endpoint on this page is SQL only, so a workspace with no configured provider serves all of them. No documents, chunks, or embeddings: entities and relations can be created directly, and a concept with no mentions is valid workspace metadata. No governance module — the authority and lifecycle labels have working defaults. No agents, runs, evals, live-data connectors, or reports. The React package imports no Next.js module and no router.

The trade-off is honest: with no documents, mentionCount is 0, docIds is empty, the manifest's documents/chunks counts are 0, and pagerank stays null until graph analytics have produced a layout version.

React components

@eli-ai/react/graph exports exactly one component, EliKnowledgeGraph, plus its two prop types (EliKnowledgeGraphProps, EliKnowledgeGraphLabels). It renders a Surface containing an optional create form, a filter input, a flat canvas of clickable concept buttons, and a side panel that loads the selected concept's neighborhood at depth 1. It calls three operations through the headless graph client: listEntities on mount, neighborhood when a concept is selected, and createEntity when the form is submitted. The filter is client-side only — it narrows the already-loaded page by name or type and issues no request.

EliKnowledgeGraph props

labelsPartial<EliKnowledgeGraphLabels>Overrides for the thirteen UI strings. Merged over the defaults, so pass only the keys you want to change.
limitnumberPage size passed to listEntities. Defaults to 100. The server clamps values above 200.
allowCreatebooleanRenders the create toggle and inline form (name, type, comma-separated aliases). Defaults to false. The submit calls POST /api/v1/entities, so the key needs kb:write.
initialEntityIdstringEntity id selected on first render, so the neighborhood panel is populated immediately.
onEntitySelect(entity: EntitySummary | EntityCard) => voidFired when a concept is chosen — from the canvas (an EntitySummary out of the list) or from a neighbor button in the side panel (an EntityCard out of the neighborhood).
onEntityCreated(entity: Entity) => voidFired after a successful create, with the created entity. The component has already closed the form, selected the new entity, and refetched the list.
onDocumentSelect(documentId: string) => voidFired when one of the source-document chips in the side panel is clicked. Those chips render only when the selected concept has docIds; wire this to your own document route.
titlestringSurface heading. Defaults to labels.title (“Knowledge graph”).
descriptionstringSurface sub-heading. Defaults to labels.description.
loadingLabelstringShown while the entity list loads. Defaults to “Loading concepts…”.
errorLabelstringShown when the entity list fails, above a retry button. Defaults to “The knowledge graph could not be loaded.”
onError(error: unknown) => voidCalled on a failed list, neighborhood, or create. Passed straight through to the underlying query and mutation hooks.
classNamestringAppended to the root element's class list after the package's own eli-root class.

No emptyLabel prop

EliKnowledgeGraphProps is Omit<CommonSurfaceProps, "emptyLabel"> plus the graph-specific props above — the empty state is driven by labels.empty instead. The neighborhood panel error and the create-form error use fixed strings and are not configurable.
EliKnowledgeGraphLabels (keys and defaults)ts
{
  title: "Knowledge graph",
  description: "Explore concepts and the evidence-backed relationships between them.",
  filter: "Filter concepts",
  type: "Type",
  create: "New concept",
  cancel: "Cancel",
  name: "Name",
  aliases: "Aliases",
  save: "Create concept",
  empty: "No concepts found",
  neighborhood: "Neighborhood",
  relations: "Relations",
  sourceDocuments: "Source documents",
}
provider wiring + the componenttsx
import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliKnowledgeGraph } from "@eli-ai/react/graph";
import "@eli-ai/react/styles.css";

// EliProvider has no API-key prop by design. Point the transport at a
// same-origin route of your own that injects the Bearer key server-side.
const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function ConceptExplorer({ workspaceId }: { workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliKnowledgeGraph
        limit={200}
        allowCreate
        labels={{ title: "Concepts", create: "Add concept" }}
        onEntitySelect={(entity) => track("concept.open", entity.id)}
        onDocumentSelect={(documentId) => openDocument("/documents/" + documentId)}
        onError={(error) => reportError(error)}
      />
    </EliProvider>
  );
}

There is no component for the manifest or for relation writes. Both are one hook away — the same generic hooks the packaged components are built on work with any graph-client method and your own markup.

headless: hub concepts from the manifesttsx
import { createGraphClient } from "@eli-ai/client/graph";
import { useEliQuery } from "@eli-ai/react/hooks";
import type { WorkspaceManifest } from "@eli-ai/contracts/graph";

export function HubConcepts() {
  const manifest = useEliQuery<WorkspaceManifest>(
    "graph.manifest",
    (context, signal) =>
      createGraphClient(context.transport).manifest({
        signal,
        ...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
      }),
  );

  if (manifest.isLoading) return <p>Loading…</p>;
  if (!manifest.data) return null;

  return (
    <ol>
      {manifest.data.hubConcepts.map((concept) => (
        <li key={concept.id}>
          {concept.name} · in-degree {concept.inDegree} · {concept.authority}
        </li>
      ))}
    </ol>
  );
}

HTTP API

Nine operations across six paths. Every request carries a Bearer key; see Authentication. Examples use https://eli.ai, whose apex 308-redirects, so -L is included on every non-GET example. A resource in another workspace is invisible under row-level security and comes back as 404, never 403.

GET/api/v1/entitiesBearer · kb:read (runs:read accepted)

Paginated canonical entities — merged duplicates resolve to their head — ordered by persisted PageRank descending, nulls last, then name, then id. pagerank is null until graph analytics have written a layout version.

Query parameters

limitintegerPage size. Must be an integer >= 1; values above 200 are clamped to 200. Default 50.
offsetintegerRows to skip. Must be an integer >= 0. Default 0.
typestringExact-match filter on the entity type, for example “system” or “person”.
Example requestbash
curl -s "https://eli.ai/api/v1/entities?limit=25&type=system" \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "entities": [
    { "id": "01KY0ZW3B8AVDR67NXSE96P81N", "name": "Order Platform", "type": "system", "pagerank": 0.0421 },
    { "id": "01KY10DDDDDDDDDDDDDDDDDDDD", "name": "northwind-ops", "type": "system", "pagerank": null }
  ],
  "total": 27,
  "limit": 25,
  "offset": 0
}

The list rows are deliberately thin

A list row is exactly id, name, type, and pagerank. It carries no aliases, mention counts, or governance labels — fetch GET /api/v1/entities/:id for the curation block, or the neighborhood endpoint for full cards.
POST/api/v1/entitiesBearer · kb:write

Create a concept by hand. Origin is recorded as manual and confidence as 1. The name itself, plus every alias, becomes an alias row, so later ingestion reuses this entity by normalized name instead of duplicating it. An unknown type is auto-registered into the ontology, best-effort.

Request body

namerequiredstringDisplay name. 1–400 characters.
typerequiredstringFree-text entity type. 1–120 characters.
aliasesstring[]Alternate surface forms. Each must be non-empty; duplicates by normalized form are dropped.
descriptionstringStored inside the entity's attrs bag and returned as the top-level description field.
Example requestbash
curl -L -X POST https://eli.ai/api/v1/entities \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Fulfillment Console","type":"system","aliases":["fulfillment-console"]}'
Responsejson
{
  "id": "01KY10KKKKKKKKKKKKKKKKKKKK",
  "name": "Fulfillment Console",
  "type": "system",
  "nameNorm": "fulfillment console",
  "origin": "manual",
  "description": null,
  "aliases": ["Fulfillment Console", "fulfillment-console"]
}

409 carries the id you collided with

The uniqueness check is on the normalized name across all live, unmerged entities in the workspace — it is not scoped by type. On a collision the response is { "error": "conflict", "message": …, "existingId": "01K…" } at status 409, so the caller can decide between reusing that entity and cancelling. An empty or whitespace-only name or type is a 400.
GET/api/v1/entities/:idBearer · kb:read (runs:read accepted)

One entity plus its curation block. The entity fields are the same shape POST and PATCH return; curation adds the governance state stored on the same row.

Example requestbash
curl -s https://eli.ai/api/v1/entities/$ENTITY_ID \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "id": "01KY0ZW3B8AVDR67NXSE96P81N",
  "name": "Order Platform",
  "type": "system",
  "nameNorm": "order platform",
  "origin": "extraction",
  "description": "Core order intake and fulfillment system.",
  "aliases": ["order-platform", "OP"],
  "curation": {
    "authority": "curated",
    "lifecycleStatus": "published",
    "ownerId": "usr_01KY0Z9M2E5Q4RS8T0V1WX2Y3Z",
    "stewardIds": [],
    "version": 4,
    "lastReviewedAt": "2026-05-02T10:14:00.000Z",
    "nextReviewAt": "2026-11-02T10:14:00.000Z",
    "supersededBy": null,
    "sourceRefs": [{ "label": "Service catalogue", "url": "https://intranet.example.com/catalogue" }],
    "editorialNote": null
  }
}

curation.editorialNote is steward-only content

The editorial note is a steward-facing field: it is stored for curators and is not meant to reach end users or an LLM context window. This endpoint returns it to any key holding kb:read. If you proxy this response to a browser or into a prompt, strip curation.editorialNote first.
PATCH/api/v1/entities/:idBearer · kb:write

Edit a concept. Works on extracted entities as well as manual ones — renaming and retyping are curator actions. A rename also registers the new name as an alias; aliases are additive; an unknown new type is auto-registered. At least one field must be present or the body is rejected with 400.

Request body

namestringNew display name. 1–400 characters.
typestringNew entity type. 1–120 characters.
aliasesstring[]Additional aliases to register. Existing aliases are kept.
descriptionstringReplaces the description stored in attrs.
Example requestbash
curl -L -X PATCH https://eli.ai/api/v1/entities/$ENTITY_ID \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"service"}'
Responsejson
{
  "id": "01KY10KKKKKKKKKKKKKKKKKKKK",
  "name": "Fulfillment Console",
  "type": "service",
  "nameNorm": "fulfillment console",
  "origin": "manual",
  "description": null,
  "aliases": ["Fulfillment Console", "fulfillment-console"]
}
DELETE/api/v1/entities/:idBearer · kb:write

Delete the concept, meaning its whole canonical cluster: the head plus every entity merged into it. Relations touching any cluster member are removed outright (cascading their evidence), mentions and alias rows are removed, the entity rows are soft-deleted, and canonical_relations is rebuilt for the head and for every neighbour head so dead edges disappear from the serve-time table.

Example requestbash
curl -L -X DELETE https://eli.ai/api/v1/entities/$ENTITY_ID \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{ "deleted": true, "id": "01KY10KKKKKKKKKKKKKKKKKKKK" }
GET/api/v1/entities/:id/neighborhoodBearer · kb:read (runs:read accepted)

Expand the canonical-relation frontier around one concept, in both edge directions, taking the highest-mention-count edges first at each hop. Returns full cards for every node reached — the seed included — plus the edges and an explicit truncated flag. A merged id resolves to its canonical head.

Query parameters

depthintegerHops to expand, 1–3. Default 1. Out-of-range or non-integer values are a 400, not clamped.
perHopCapintegerMaximum new nodes admitted per hop, 1–100. Default 25.
totalCapintegerMaximum total nodes in the result, 1–500. Default 200.
Example requestbash
curl -s "https://eli.ai/api/v1/entities/$ENTITY_ID/neighborhood?depth=2&perHopCap=10" \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "entity": {
    "id": "01KY0ZW3B8AVDR67NXSE96P81N",
    "name": "Order Platform",
    "type": "system",
    "aliases": ["order-platform", "OP"],
    "mentionCount": 42,
    "topRelations": ["Order Platform —depends_on→ northwind-ops"],
    "docIds": ["01KY10AAAAAAAAAAAAAAAAAAAA"],
    "authority": "curated",
    "lifecycleStatus": "published"
  },
  "nodes": [
    {
      "id": "01KY10DDDDDDDDDDDDDDDDDDDD",
      "name": "northwind-ops",
      "type": "system",
      "aliases": [],
      "mentionCount": 18,
      "topRelations": ["Order Platform —depends_on→ northwind-ops"],
      "docIds": ["01KY10AAAAAAAAAAAAAAAAAAAA"],
      "authority": "machine_extracted",
      "lifecycleStatus": "published"
    }
  ],
  "edges": [
    {
      "src": "01KY0ZW3B8AVDR67NXSE96P81N",
      "type": "depends_on",
      "dst": "01KY10DDDDDDDDDDDDDDDDDDDD",
      "mentionCount": 7
    }
  ],
  "truncated": false
}

Card field shapes worth knowing before you render them

aliases is capped at 5 and docIds at 5 (the most-mentioned documents first). topRelations is capped at 8 and each item is a rendered sentence, not a relation type — the literal format is <source name> —<type>→ <destination name>, for example Order Platform —depends_on→ northwind-ops. Treat it as display text; use edges when you need structure. mentionCount is summed across the whole merged cluster.

nodes always includes the seed entity, so nodes.length is at least 1 for a concept with no edges. truncated is set when a cap was reached, so a client can say "there is more" instead of silently showing a partial graph.

POST/api/v1/relationsBearer · kb:write

Create a directed, typed edge between two existing live entities, then rebuild canonical_relations for both endpoints' heads so the edge is visible to reads immediately. Re-asserting an edge that already exists promotes it rather than erroring. An unknown relation type is auto-registered, best-effort.

Request body

srcEntityIdrequiredstringSource entity id. Must be live.
dstEntityIdrequiredstringDestination entity id. Must be live, and different from the source.
typerequiredstringRelation type, for example depends_on. 1–120 characters.
evidencestringOptional quote backing the edge. Stored as a relation_evidence row with no source document.
Example requestbash
curl -L -X POST https://eli.ai/api/v1/relations \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"srcEntityId":"01KY0ZW3…","dstEntityId":"01KY10KK…","type":"depends_on"}'
Responsejson
{
  "relationId": "01KY10MMMMMMMMMMMMMMMMMMMM",
  "affectedCanonicalIds": ["01KY0ZW3B8AVDR67NXSE96P81N", "01KY10KKKKKKKKKKKKKKKKKKKK"]
}

Domain and range are enforced on write

If the relation type has srcTypes or dstTypes registered in relation_types, the endpoint checks the endpoints' entity types against them and rejects a violation with a 400 naming the offending side and the allowed types. An empty side is unconstrained, which is the default for types created implicitly. A missing endpoint is a 404; a self-edge is a 400. Posting a triple that already exists is not an error: the existing edge is promoted to asserted authority and its id is returned with the usual 201, so this endpoint is safe to replay.
DELETE/api/v1/relations/:idBearer · kb:write

Remove one relation, cascading its evidence rows, and rebuild canonical_relations for both endpoints' canonical heads. An unknown id is a 404.

Example requestbash
curl -L -X DELETE https://eli.ai/api/v1/relations/$RELATION_ID \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{ "deleted": true, "id": "01KY10MMMMMMMMMMMMMMMMMMMM" }

The delete responses are acknowledgements only

Both delete endpoints return exactly { deleted, id }. The server computes richer results — the affected canonical heads, and for entities the number of relations and mentions removed — but neither route puts them on the wire. The affectedCanonicalIds field is optional in the contract types for that reason; do not depend on it after a delete. It is returned by POST /api/v1/relations.
GET/api/v1/manifestBearer · kb:read (runs:read accepted)

One machine-readable snapshot of the workspace: corpus and graph counts, the distribution by type, authority, lifecycle and document verification, the top 25 hub concepts by directed in-degree over the canonical graph, and review staleness. Pure SQL, no model call. Only published concepts are eligible to be hub concepts.

Example requestbash
curl -s https://eli.ai/api/v1/manifest \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "generatedAt": "2026-07-21T09:15:00.000Z",
  "workspace": { "id": "ws_01KY0Z00000000000000000000", "name": "Northwind Ops" },
  "counts": {
    "documents": 128,
    "chunks": 3410,
    "entities": 743,
    "relations": 1289,
    "entitiesByType": { "system": 41, "metric": 22, "policy": 17 },
    "relationsByType": { "governed_by": 58, "owned_by": 44 },
    "byAuthority": { "machine_extracted": 601, "asserted": 92, "curated": 38, "certified": 12 },
    "byLifecycle": { "draft": 4, "pending_review": 2, "published": 724, "deprecated": 9, "superseded": 4 },
    "documentsByVerification": { "unverified": 96, "verified": 24, "certified": 6, "deprecated": 2 }
  },
  "hubConcepts": [
    {
      "id": "01KY0ZW3B8AVDR67NXSE96P81N",
      "name": "Order Platform",
      "type": "system",
      "summary": "Core order intake and fulfillment system.",
      "authority": "certified",
      "lifecycleStatus": "published",
      "version": 6,
      "inDegree": 31,
      "outDegree": 12,
      "mentionCount": 87,
      "lastReviewedAt": "2026-07-14T10:02:11.000Z",
      "ownerId": "usr_01KY0Z9M2E5Q4RS8T0V1WX2Y3Z"
    }
  ],
  "staleness": { "overdueReviews": 3, "neverReviewed": 640 }
}

Data elements

Atlas owns ten tables. Every one of them carries a workspace_id column and a tenant-isolation row-level-security policy that constrains both reads and writes to the workspace of the current transaction, so a cross-workspace id is simply not visible rather than forbidden. Ids are ULID strings.

TableWhat one row isColumns a consumer cares about
entitiesOne concept. A canonical head, or a member merged into one. Soft-deleted, never hard-deleted, by the delete endpoint.id · name · name_norm · type · attrs.description · aliases (via entity_aliases) · origin (extraction | manual | wikilink-unresolved) · canonical_id · merged_into · confidence · authority · lifecycle_status · owner_id · steward_ids · version · deleted_at
entity_aliasesOne surface form an entity answers to. A manual create stores the entity's own name here as well, which is how later ingestion finds and reuses it.entity_id · alias · alias_norm (unique per workspace) · source · merge_id
relationsOne directed, typed edge at the mention level. Manual creates land here with authority 'asserted'.src_entity_id · dst_entity_id · type · confidence · authority · assertion_rank · deleted_at (live triple is unique per workspace)
relation_evidenceThe provenance of an edge: one quote that grounds it, optionally located in a document and chunk.relation_id · quote · doc_id · chunk_id · start_offset · end_offset · extraction_run_id
mentionsThe provenance of a concept: one occurrence of an entity in a document, with the exact surface text.entity_id · doc_id · chunk_id · surface_text · start_offset · end_offset · link_method (extraction | alias_exact | name_exact | wikilink | frontmatter | adjudication | manual) · alias_id
canonical_relationsThe materialized aggregate over relations, rolled up to canonical heads. The only edge table read at serve time; rebuilt for the touched heads by relation create/delete and by entity delete (a rename or retype needs no rebuild — the rollup is id-keyed).src_canonical · relation_type · dst_canonical · mention_count · sample_evidence_ids
entity_typesOne registered entity type in the workspace vocabulary. Auto-registered when a create or retype uses an unknown type.name (part of the primary key) · description · examples
relation_typesOne registered relation type, optionally with domain and range constraints enforced on manual edge creation.name (part of the primary key) · description · src_types · dst_types
graph_layoutOne node's analytics output for one layout version — position, community, PageRank, degree. Where the list endpoint's pagerank comes from.layout_version · entity_id · x · y · community_id · pagerank · degree (workspace_meta.current_layout_version selects the live version)
entity_revisionsOne append-only history entry for a concept. Written by every Atlas create, update, and delete; never updated or deleted, and it has no foreign key so it survives entity removal.entity_id · version · change_kind · snapshot · diff · activity · changed_by · note · created_at

Atlas also reads two tables it does not own: documents (for the ACL columns and to resolve docIds) and chunks (referenced by mentions and evidence). Three more Atlas-adjacent tables exist but have no v1 endpoint on this page — merge_log, extraction_runs, and rejection_tombstones.

Two visibility rules that shape every read

Serving exclusion. Soft-deleted entities and concepts in draft or pending_review are never served on cards or neighborhoods — and neither are edges to them. deprecated and superseded concepts stay retrievable, labelled rather than hidden.

Document ACL. An entity that has document mentions is readable only when at least one of those documents is readable by the caller; a restricted-only entity is omitted entirely, name and id included. An entity with no mentions — a hand-authored vocabulary entry — is workspace metadata and always visible. Canonical edges follow the same rule through their evidence documents; a manually asserted edge with no evidence rows stays visible.

Feature map

Six feature areas make up the module. Each names the concrete modules and tables that deliver it, and those names reappear in the code map and internals below, so every claim on this page is traceable to the code that enforces it.

Feature areaWhat it providesDelivered bySurfaces
Canonical concepts and aliasesTyped entities with normalized-name identity, additive aliases, origins (manual, extraction, wikilink), and soft deletion.createEntity / updateEntity / deleteEntity in src/server/graph/manual.ts; normName in src/server/graph/shared.ts; the entities and entity_aliases tablesPOST / PATCH / DELETE on /api/v1/entities · EliKnowledgeGraph create form · graph SDK client
Typed relations with evidenceDirected, typed edges with optional grounding quotes, domain and range checks, and replay-safe re-assertion.createRelation / deleteRelation in manual.ts; checkRelationConstraint in src/server/graph/constraints.ts; the relations and relation_evidence tablesPOST /api/v1/relations · DELETE /api/v1/relations/:id
Serve-time readsEntity cards, bounded frontier expansion, the thin PageRank-ordered list, and name resolution — all pure SQL under the document ACL.entityCard / expandFrontier / findEntities in src/server/graph/query.ts; listEntitiesForApi in src/server/query/entities.tsGET /api/v1/entities, /entities/:id, the neighborhood · EliKnowledgeGraph panel · MCP graph tools
Merge resolution and canonical rollupNon-destructive, invertible merges plus the materialized edge table every read is served from.mergeEntities / unmergeEntity in src/server/graph/resolution.ts; rebuildCanonicalForIds in shared.ts; the canonical_relations and merge_log tablesNo v1 endpoint of its own — every REST read resolves merged ids to their canonical head
Extraction and graph analyticsThe write side that fills the graph from documents — model-driven extraction plus deterministic wikilink and co-occurrence lanes — and the PageRank/community analytics behind the list order.src/server/graph/extraction.ts with resolveModel from src/server/ai/registry.ts; src/server/graph/analytics.ts; the graph_layout tableBackground jobs enqueued by ingestion · pagerank on GET /api/v1/entities
Workspace manifestOne machine-readable snapshot: counts, governance distributions, hub concepts by in-degree, review staleness.buildManifestForContentAcl in src/server/graph/manifest.tsGET /api/v1/manifest · the MCP resource eli://workspace/manifest

System architecture

The topology separates the write side (manual curation, extraction, merges) from the serve side (pure SQL reads) with canonical_relations as the hinge between them. Rectangles are API routes and services, cylinders are tables, and the one rounded node is the only model call in the module — extraction on the write side. Nothing on the serve side calls a model.

Atlas — system architecture

Writers (manual.ts, extraction.ts, resolution.ts) all converge on rebuildCanonicalForIds, which maintains canonical_relations — the only edge table the serve side reads. Analytics runs offline and persists PageRank into graph_layout, which the list read joins.

Rendering diagram

Downloads

Concepts

  • Atlas architecture
  • Write side
  • Serve side — pure SQL
  • REST surface — src/app/api/v1
  • Modules
  • Atlas
  • Analytics

Keywords

  • entities routes (API)
  • relations routes (API)
  • manifest route (API)
  • manual.ts — hand-authored writes (service)
  • extraction.ts — ingestion extraction (service)
  • resolution.ts — merge and unmerge (service)
  • shared.ts — rebuildCanonicalForIds (service)
  • manifest.ts — workspace manifest (service)
  • analytics.ts — PageRank and Louvain (service)
  • persist-layout.ts — layout versions (service)
  • constraints.ts — domain and range checks (service)
  • query.ts — entityCard + expandFrontier (service)
  • content-acl.ts — document ACL predicate (service)
  • store: tables
  • store: table
  • listEntitiesForApi (service)
  • query/entities.ts
  • model
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:31:12.961Z

Source hash: 37b027cfaba9e6f3e2e9767ff096741a0f978c882b0ae0ddca6ad61c029cf0d9

Metadata payload hash: 401acaaf4a4935351e385a125851ebfc934d70ef76cb10d97931234771c2195e

Canonical appearance

src/app/(docs)/docs/modules/atlas/page.tsx:65 route /docs/modules/atlas

All appearances

  • canonicalsrc/app/(docs)/docs/modules/atlas/page.tsx:65 route /docs/modules/atlas

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: atlas-system-architecture-37b027cf.json

How to read the Atlas system architecture

  1. Find the hinge table: canonical_relations sits between the write and read halves: every writer that changes edges or heads rebuilds it, and no read ever touches raw relations.
  2. Trace a manual write: Entity and relation routes call manual.ts, which validates domain and range through constraints.ts, writes the base tables, auto-registers unknown types, and repairs the rollup.
  3. Trace the extraction lane: extraction.ts is the one model-calling component; it resolves the workspace-configured model and writes entities, relations, mentions, and evidence with tombstone-aware idempotency.
  4. Follow PageRank to the list: analytics.ts computes PageRank and communities offline; persist-layout.ts versions them into graph_layout, and listEntitiesForApi joins the current layout version.
Trust boundary
All reads and writes run inside workspace transactions under row-level security, and serve-time reads additionally apply the document ACL so restricted-document-derived structure is invisible.
Durable state
entities, entity_aliases, relations, relation_evidence, mentions, canonical_relations, ontology tables, merge_log, and versioned graph_layout rows.

Failure paths

  • A rollup rebuild missed after a write would serve stale edges — every writer calls it in-transaction
  • Domain or range violations reject manual writes with a 400 naming the offending side
  • Extraction model failures leave the deterministic wikilink and co-occurrence lanes intact
  • Analytics never running leaves pagerank null rather than wrong

Signals

  • canonical_relations row count versus live relations
  • extraction_runs outcomes per document
  • merge_log volume and unmerge rate
  • graph_layout layout_version freshness

Where the logic lives

The code map is the module boundary in file terms. Every path is real; table names are stated in the notes because tables are defined in the schema module, not in files of their own.

ComponentKindLives atNotes
Entity collection routesAPIsrc/app/api/v1/entitiesGET thin list (kb:read, legacy runs:read accepted) and POST manual create (kb:write).
Entity item routesAPIsrc/app/api/v1/entities/[id]GET card + curation block, PATCH curator edits, DELETE whole-cluster removal.
Neighborhood routeAPIsrc/app/api/v1/entities/[id]/neighborhoodBounded frontier expansion, depth 1–3, with an explicit truncated flag.
Relations routesAPIsrc/app/api/v1/relationsPOST at the collection, DELETE under the id segment; both rebuild the rollup for touched heads.
Manifest routeAPIsrc/app/api/v1/manifestOne snapshot per call; pure SQL.
Manual graph writesservicesrc/server/graph/manual.tscreateEntity / updateEntity / deleteEntity / createRelation / deleteRelation; typed errors map to 400, 404, 409.
Serve-time readsservicesrc/server/graph/query.tsentityCard, entityCards, expandFrontier, findEntities; canonical_relations is the only edge table read.
API entity listingservicesrc/server/query/entities.tslistEntitiesForApi joins graph_layout at workspace_meta.current_layout_version for pagerank.
Merge resolutionservicesrc/server/graph/resolution.tsmergeEntities / unmergeEntity with cycle guards; every step logged in merge_log and invertible.
Canonical rollup repairservicesrc/server/graph/shared.tsrebuildCanonicalForIds, normName, and the alias-eligibility rule shared by every writer.
Ontology constraintsservicesrc/server/graph/constraints.tsDomain and range checks from relation_types; unknown types auto-register through src/server/graph/ontology-admin.ts.
Ingestion extraction writerservicesrc/server/graph/extraction.tsModel-driven extraction plus deterministic wikilink and co-occurrence lanes; writes mentions and evidence with offsets.
Extraction model slotmodelsrc/server/ai/registry.tsresolveModel selects the workspace-configured provider model; only the extraction write path calls one.
Analytics and layoutservicesrc/server/graph/analytics.tsPower-iteration PageRank and Louvain communities, persisted as layout versions by src/server/graph/persist-layout.ts into graph_layout.
Graph tablesstoresrc/server/db/schema.tsentities, entity_aliases, relations, relation_evidence, mentions, canonical_relations, entity_types, relation_types, graph_layout, entity_revisions — all tenant-scoped by RLS.
EliKnowledgeGraph surfaceUIpackages/react/src/graph/index.tsxCanvas, filter, create form, and the depth-1 neighborhood panel.
Graph clientSDKpackages/client/src/graph.tscreateGraphClient — listEntities, getEntity, neighborhood, manifest, createEntity, createRelation, deletes.
Wire contractsSDKpackages/contracts/src/graph.tsEntity, EntitySummary, EntityCard, NeighborhoodResult, WorkspaceManifest.

Primary runtime flow

The highest-value read is the neighborhood expansion — it exercises canonicalization, the rollup, the caps, and the ACL in one request. Message labels use the operation names from the HTTP API section above.

Atlas — primary runtime flow

getEntityNeighborhood canonicalizes the seed, expands the frontier hop by hop over canonical_relations taking highest-mention edges first, then hydrates full cards for every reached node under the document ACL.

Rendering diagram

Downloads

Concepts

  • Atlas runtime flow
  • Modules
  • Atlas

Keywords

  • API consumer (SDK or curl)
  • GET :id/neighborhood (API)
  • canonical_relations (store)
  • entities + entity_aliases (store)
  • requireApiKey + content ACL (service)
  • mentions + documents (store)
  • workspace plus content-ACL context
  • entityCard resolves the seed
  • canonicalize a merged id to its head
  • expandFrontier from the canonical seed
  • hydrate full cards for every reached node
  • nodes, edges, truncated flag
  • getEntityNeighborhood with depth, perHopCap, totalCap
  • check kb:read (runs:read accepted) and graph:read
  • read both edge directions, highest mentionCount first
  • admit nodes under perHopCap and totalCap, set truncated
  • graph/query.ts
  • /api/v1/entities
  • service
  • roll up mentionCount, docIds, topRelations under the document ACL
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:31:08.402Z

Source hash: d86a06d2e6529e931fbdcb7b5b062c24d68e035b452833bb7fbd4d3b4399e95e

Metadata payload hash: 66abd2fc85eedf19353dc44c4553886d52f95578edf0d194a6a17d4d2381cd1e

Canonical appearance

src/app/(docs)/docs/modules/atlas/page.tsx:131 route /docs/modules/atlas

All appearances

  • canonicalsrc/app/(docs)/docs/modules/atlas/page.tsx:131 route /docs/modules/atlas

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: atlas-primary-runtime-flow-d86a06d2.json

How to read the Atlas primary runtime flow

  1. Guard first: The route resolves the key to a workspace, checks kb:read (or the legacy runs:read grant), re-checks the graph:read capability for delegated keys, and builds the content-ACL context.
  2. Canonicalize before expanding: A merged id resolves to its canonical head before any edge is read, so callers can hold stale ids safely.
  3. Watch the caps: Each hop admits at most perHopCap new nodes and the result at most totalCap; hitting either sets truncated instead of silently cutting output.
  4. Hydration is where policy lands: Cards roll up aliases, mentions, and docIds; serving exclusion and the document ACL filter both nodes and counterparts at this step.
Trust boundary
Scope and capability checks happen before any query; the document ACL is applied inside the SQL so restricted-only entities and their edges never reach the response.
Durable state
Nothing is written — the flow is a pure read over entities, aliases, mentions, canonical_relations, and graph_layout.

Failure paths

  • Out-of-range depth or caps are a 400, not clamped
  • An unknown or cross-workspace id is a 404 under RLS
  • A restricted-only seed entity is indistinguishable from a missing one
  • Hitting totalCap returns truncated: true so clients can offer expansion

Signals

  • Neighborhood latency by depth
  • truncated rate by workspace
  • 404 rate on entity reads (stale ids)
  • pagerank null share on the list read

Internals and invariants

Canonical concepts and aliases

An entity's identity is its normalized name — NFKC, whitespace-collapsed, lowercased — computed by normName. A manual create registers the display name and every alias as alias rows, which is how later ingestion reuses the concept instead of duplicating it. Deletes are soft: the row keeps its id, and the serve side excludes it.

  • Invariant — Normalized-name uniqueness across live, unmerged entities: a colliding create is a 409 carrying the existing id, enforced in src/server/graph/manual.ts.
  • Invariant — Only multi-token or six-plus-character normalized names may become cross-document aliases, so short speaker labels cannot fabricate structure, enforced by isAliasEligible in src/server/graph/shared.ts.

Typed relations with evidence

A relation is a directed triple that must connect two live entities. When the relation type declares domain or range constraints, the endpoints' entity types are checked on write. Re-asserting an existing triple promotes it rather than erroring, which makes the endpoint safe to replay.

  • Invariant — Domain and range constraints reject a violating manual write with a 400 naming the offending side; extraction skips and counts the same violations, enforced by src/server/graph/constraints.ts.
  • Invariant— Relation writes and deletes rebuild the canonical rollup for both endpoints' heads in the same operation, so reads never see a stale edge, enforced through src/server/graph/manual.ts.

Serve-time reads

Reads never traverse raw relations. Cards and frontiers come from canonical_relations, hydrated with aliases, mention rollups, and rendered top-relation sentences; the thin list joins persisted PageRank. All of it is short workspace transactions of plain SQL.

  • Invariant — Soft-deleted, draft, and pending-review concepts are never served on cards or frontiers, and neither are edges to them, enforced by the serving-exclusion predicate in src/server/graph/query.ts.
  • Invariant— An entity with document mentions is readable only when at least one of those documents passes the caller's content ACL, enforced in src/server/graph/query.ts via src/server/authz/content-acl.ts.
  • Invariant— The list endpoint's pagerank comes only from the persisted layout version selected by workspace_meta, never computed inline, enforced in src/server/query/entities.ts.

Merge resolution and canonical rollup

A merge is a pointer plus a transactional recompute: merged_into is set, canonical ids are recalculated for the source cluster, aliases are repointed and tagged with the merge id, and the rollup is rebuilt for both heads. Unmerge reverses every step from the merge_log row.

  • Invariant — Merges are cycle-guarded and non-destructive: every step is recorded and invertible, so unmerge restores the exact pre-merge graph, enforced in src/server/graph/resolution.ts.
  • Invariant — canonical_relations is repaired by delete-and-reinsert closed under one predicate for the touched heads, aggregating mention counts with bounded evidence samples, enforced by rebuildCanonicalForIds in src/server/graph/shared.ts.

Extraction and graph analytics

Extraction runs per document from queued jobs: the model proposes entities and relations, deterministic wikilink and co-occurrence lanes add structure without a model, and every mention lands with character offsets so provenance is exact. Analytics computes PageRank and communities in memory after one short read and persists them as a new layout version.

  • Invariant — Every extracted fact carries locatable provenance — mention and evidence rows store the document, chunk, and offsets, written by src/server/graph/extraction.ts.
  • Invariant— Rejected extractions leave tombstones so a re-run cannot resurrect a curator's rejection, enforced via the tombstone signatures in src/server/graph/shared.ts.

Workspace manifest

The manifest is one read: corpus and graph counts, distributions by type, authority, lifecycle, and verification, hub concepts ranked by directed in-degree over the canonical graph, and staleness counters. Only published concepts are hub-eligible.

  • Invariant — Hub concepts are computed over canonical_relations restricted to live, published entities, enforced in src/server/graph/manifest.ts.

For a self-hosting team the safe extension points are the ontology and the read facade: register entity and relation types (with domain and range constraints) rather than forking the writers, drive writes through the plugin data facade so constraints and ACLs still apply, and re-run analytics on your own cadence — a new layout version swaps in atomically. The normalized-name identity, the canonical rollup, and the serving-exclusion rules are load-bearing and not configurable.

How it composes

Atlas is usable on its own, and every other module that touches the graph does so through the same tables and the same serve-time functions this page documents.

  • Intake the biggest gain. With documents in the workspace, extraction writes entities, aliases, relations, mentions, and evidence for you, and the fields that are empty in a hand-built graph fill in: mentionCount, docIds, edge mentionCount, and the manifest's document and chunk counts. Without it, nothing breaks — you author the graph through POST /entities and POST /relations.
  • Warrant required for governance writes. The authority and lifecycle_status columns live on the Atlas entities row and Atlas serves them on every card and in the curation block, but Atlas has no endpoint to change them. Certifying, setting an owner, stamping a review, deprecating, the revision history endpoint, change requests, and the SKOS export are all Warrant. Without Warrant the defaults hold — manual creates are asserted, extraction is machine_extracted, everything is published — and the serving rules above still apply.
  • Lineage — reads Atlas from the document side: which concepts a document produced, and how many live relations its evidence grounds. It needs documents, so it is meaningful only alongside Intake.
  • Lens — the retrieval pipeline's graph-fusion stage calls the very same frontier expansion this module exposes, to surface evidence that is structurally rather than textually related. Atlas does not need Lens; Lens gets materially better with a populated Atlas. See Retrieval pipeline.
  • Conduit — binds a governed live query to an entity id or to an entity type, so Atlas is the addressing scheme for live data. The binding rows live in Conduit's table and reference Atlas ids; Atlas itself neither reads nor needs them.
  • Crucible — grades concept recall against entity ids on golden items. Optional, and evaluation-only.
  • Ports — exposes the same reads over MCP (kb_entity_lookup, kb_entity_get, kb_graph_query, and the eli://workspace/manifest resource) for agents that speak that protocol instead of REST. Same server functions, different envelope.

Related reading

Entity graph explains the modelling decisions behind canonicalization and merges; Manifest covers the manifest as an agent bootstrap document; JavaScript & React SDK covers the transport, provider, and error types shared by every module.

Extending Atlas

Atlas exposes no contribution point of its own — the graph is read and written through the plugin data facade instead: ctx.data.atlas covers entity search, entity cards, frontier expansion and the documents mentioning an entity under graph:read, and entity and relation writes under graph:write, so ontology constraints and content ACLs apply exactly as they do for the REST surface. Atlas emits graph.extraction.completed, entity.created, entity.merged and entity.deleted. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.