v0.73.1 · Look Before You Write

The Knowledge Graph for TypeScript

Define your ontology with Zod. Query with a fluent builder. Extend it at runtime when your agents need more.

GraphUserPostGroupRoleZodTypes

Define. Connect. Query. Evolve.

Your schema is your database. Your types are your queries.

import { defineNode, defineEdge, defineGraph, createStore } from "@nicia-ai/typegraph";
import { z } from "zod";

// 1. Define your ontology
const Person = defineNode("Person", {
  schema: z.object({ name: z.string(), role: z.string() })
});
const Project = defineNode("Project", {
  schema: z.object({ name: z.string(), status: z.string() })
});
const worksOn = defineEdge("worksOn");

const graph = defineGraph({
  id: "my_app",
  nodes: { Person: { type: Person }, Project: { type: Project } },
  edges: { worksOn: { type: worksOn, from: [Person], to: [Project] } }
});

// 2. Query with full type safety
const results = await store.query()
  .from("Person", "p")
  .whereNode("p", p => p.role.eq("Engineer"))
  .traverse("worksOn", "e")
  .to("Project", "proj")
  .select(ctx => ({ person: ctx.p.name, project: ctx.proj.name }))
  .execute();
import { defineNode, searchable, embedding } from "@nicia-ai/typegraph";
import { z } from "zod";

// 1. Declare semantic + fulltext fields right in the Zod schema
const Document = defineNode("Document", {
  schema: z.object({
    title: searchable({ language: "english" }),
    body:  searchable({ language: "english" }),
    embedding: embedding(1536),
  })
});

// 2. Hybrid retrieval: BM25 fulltext + vector similarity, fused with RRF
const hits = await store.search.hybrid("Document", {
  limit: 10,
  vector:   { fieldPath: "embedding", queryEmbedding: await embed(q) },
  fulltext: { query: q },
});

// 3. Or compose fulltext into any graph traversal — authorised search,
//    multi-tenant filters, anything you can already express in a query.
const scoped = await store.query()
  .from("User", "u").whereNode("u", u => u.id.eq(currentUserId))
  .traverse("canRead", "e").to("Document", "d")
  .whereNode("d", d => d.$fulltext.matches(query, 10))
  .select(ctx => ctx.d).execute();
import { defineGraphExtension } from "@nicia-ai/typegraph";

// 1. An agent proposes a typed kind from observed data.
const proposal = defineGraphExtension({
  nodes: {
    Paper: {
      description: "An academic paper inferred from the corpus",
      properties: {
        title: { type: "string", minLength: 1 },
        doi:   { type: "string", minLength: 1 },
        year:  { type: "number", int: true, min: 1900, max: 2100 },
      },
      unique: [{ name: "paper_doi_unique", fields: ["doi"] }],
    },
  },
});

// 2. Operator approves; commit atomically as a new schema version.
const evolved = await store.evolve(proposal);

// 3. Read and write the new kind dynamically — no codegen, no redeploy.
const papers = evolved.getNodeCollection("Paper")!;
await papers.create({
  title: "Attention Is All You Need",
  doi:   "10.5555/3295222.3295349",
  year:  2017,
});
import { createStore, asRecordedInstant } from "@nicia-ai/typegraph";

// 1. Turn on recorded/system-time capture for committed writes.
const store = createStore(graph, backend, { history: true });

// 2. Capture a deterministic recorded-time anchor after an agent decides.
await store.nodes.Decision.create({ answer: "approve source A" });
const decisionTime = await store.recordedNow();
if (decisionTime === undefined) throw new Error("no recorded history yet");

// 3. Later, replay the exact graph the agent saw.
const replay = store.asOfRecorded(decisionTime);
const sameAnswer = await replay
  .query()
  .from("Decision", "d")
  .select(ctx => ctx.d.answer)
  .first();

// Valid time and recorded time can be pinned independently.
const whatWeKnewThen = store
  .asOf("2026-01-01T00:00:00.000Z")
  .asOfRecorded(asRecordedInstant("2026-02-01T00:00:00.000Z"));

The graph lifecycle

Stop stitching graph tools together.

Keep schema, search, reasoning, merge, and history in one typed system.

From the blog

Notes from building TypeGraph

All posts

FAQ

Frequently asked questions

What is TypeGraph?

TypeGraph is a TypeScript-first embedded knowledge graph library. It brings property graph modeling and practical ontology support to your existing SQLite or PostgreSQL database — define nodes and edges with Zod, query with a fluent TypeScript API, and keep graph and app data in one deployment.

Do I need a separate graph database to use TypeGraph?

No. TypeGraph runs on the SQL database you already have — SQLite or PostgreSQL — instead of a dedicated graph service. You can start on SQLite and move to PostgreSQL later without changing your graph definition.

Does TypeGraph support vector or hybrid search?

Yes. TypeGraph runs vector and hybrid (BM25 + vector) search across every backend it supports — pgvector and libSQL/Turso native vectors on PostgreSQL and SQLite respectively.

Can I change the schema at runtime?

Yes. TypeGraph supports runtime schema evolution, including agent-proposed changes submitted as JSON, without a redeploy. See the Graph Extensions guide for details.

Does TypeGraph support historical or temporal queries?

Yes. TypeGraph reconstructs valid-time and recorded-time history with bitemporal reads, which is useful for auditable agent memory, decision replay, and forensics.

What license is TypeGraph released under?

TypeGraph is open source under the MIT license.

Stay in the loop

Occasional updates on new features, guides, and releases. No spam.