Getting Started with TypeDB
Most databases store flat records. Tables (or collections), rows, queries that match conditions. This works for most applications. It breaks down when the relationships between things matter as much as the things themselves.
Consider a software company. People belong to teams. People are assigned to projects with specific roles. Teams own projects. Some relationships are explicit (Alice is on Engineering), others are implicit (Alice has indirect exposure to a project because her teammate is assigned to it). In a relational database, you'd model this with junction tables and multi-way JOINs. The queries get complex fast, and there's no built-in way to derive facts that were never explicitly inserted.
TypeDB takes a different approach. It's built on type theory: entity types, relation types, and attribute types in an expressive schema, with the database enforcing logical constraints at every level. Its query language, TypeQL, supports pattern matching across arbitrarily deep relationships. The defining feature is inference: define rules, and TypeDB materializes new facts from existing data at query time. You never inserted that Alice has indirect exposure to Project X, but TypeDB can figure it out.
Let's build a knowledge graph for a software company in one TypeScript file, from schema definition to inference rules that derive facts you never inserted. Run locally or provision a managed instance on Layerbase Cloud.
Contents
- Create a TypeDB Instance
- Set Up the Project
- Connect and Create a Database
- Define the Schema
- Insert the Data
- Pattern Matching Queries
- Multi-Hop Traversals
- Inference Rules
- The SQL Equivalent
- When to Use TypeDB
- Wrapping Up
Create a TypeDB Instance
Local with SpinDB
SpinDB gets TypeDB running locally without Docker or manual binary management. (What is SpinDB?)
Install SpinDB globally:
npm i -g spindb # npm
pnpm add -g spindb # pnpmOr run it directly without installing:
npx spindb create typedb1 -e typedb --start # npm
pnpx spindb create typedb1 -e typedb --start # pnpmIf you installed globally, create and start a TypeDB instance:
spindb create typedb1 -e typedb --startSpinDB downloads the TypeDB binary for your platform, configures it, and starts the server. Verify it's running:
spindb url typedb1localhost:1729Leave the server running. We'll connect to it from TypeScript in the next section.
Layerbase Cloud
Rather skip the local install? Layerbase Cloud has managed TypeDB instances. Pick TypeDB from the engine list and grab your connection details from the Quick Connect panel.
Cloud instances use TLS, so the connection code is slightly different:
const driver = await TypeDB.coreDriver('cloud.layerbase.dev:11010', {
tlsRootCAPath: undefined, // uses system CA store
})Everything else in this guide works identically whether you're running locally or on Layerbase Cloud. Just swap in your connection details.
Set Up the Project
mkdir typedb-knowledge-graph && cd typedb-knowledge-graph
pnpm init
pnpm add typedb-driver
pnpm add -D tsx typescriptCreate a file called knowledge-graph.ts. All the code in this post goes into that one file.
Connect and Create a Database
A TypeDB server hosts multiple databases, each with its own schema and data. You interact through sessions (schema or data) and transactions within those sessions.
import { TypeDB, SessionType, TransactionType } from 'typedb-driver'
const driver = await TypeDB.coreDriver('localhost:1729')
const DB_NAME = 'company'
// Clean up from previous runs
const existing = await driver.databases.all()
for (const db of existing) {
if (db.name === DB_NAME) {
await db.delete()
}
}
await driver.databases.create(DB_NAME)
console.log(`Created database "${DB_NAME}"`)Define the Schema
TypeDB schemas use TypeQL. You define entity types (things), relation types (connections), and attribute types (properties). Every type explicitly declares what it owns and what roles it can play.
const schemaSession = await driver.session(DB_NAME, SessionType.SCHEMA)
const schemaTx = await schemaSession.transaction(TransactionType.WRITE)
await schemaTx.query.define(`
define
# Attribute types
name sub attribute, value string;
email sub attribute, value string;
department sub attribute, value string;
status sub attribute, value string;
role-name sub attribute, value string;
# Entity types
person sub entity,
owns name,
owns email,
plays membership:member,
plays assignment:contributor;
team sub entity,
owns name,
owns department,
plays membership:group;
project sub entity,
owns name,
owns status,
plays assignment:target;
# Relation types
membership sub relation,
relates member,
relates group;
assignment sub relation,
owns role-name,
relates contributor,
relates target;
`)
await schemaTx.commit()
await schemaSession.close()
console.log('Schema defined')Already different from SQL. A relational foreign key is just an integer column with no semantic meaning. Here, the schema declares that membership connects a member (must be a person) to a group (must be a team). You can't accidentally insert a membership between two projects. The type system won't let you.
Notice that assignment owns an attribute (role-name). Relations can have properties. Relational databases struggle with this (you'd need a junction table with extra columns), and property graphs handle it inconsistently.
Insert the Data
Open a data session and insert six people, three teams, four projects, and their relationships:
const dataSession = await driver.session(DB_NAME, SessionType.DATA)
const insertTx = await dataSession.transaction(TransactionType.WRITE)
// Insert people
await insertTx.query.insert(`
insert
$alice isa person, has name "Alice", has email "alice@example.com";
$bob isa person, has name "Bob", has email "bob@example.com";
$carol isa person, has name "Carol", has email "carol@example.com";
$dave isa person, has name "Dave", has email "dave@example.com";
$eve isa person, has name "Eve", has email "eve@example.com";
$frank isa person, has name "Frank", has email "frank@example.com";
`)
// Insert teams
await insertTx.query.insert(`
insert
$eng isa team, has name "Engineering", has department "Product";
$data isa team, has name "Data Science", has department "Research";
$platform isa team, has name "Platform", has department "Infrastructure";
`)
// Insert projects
await insertTx.query.insert(`
insert
$api isa project, has name "API Redesign", has status "active";
$ml isa project, has name "ML Pipeline", has status "active";
$migration isa project, has name "Cloud Migration", has status "planning";
$dashboard isa project, has name "Analytics Dashboard", has status "active";
`)
// Insert memberships (person -> team)
await insertTx.query.insert(`
match
$alice isa person, has name "Alice";
$bob isa person, has name "Bob";
$carol isa person, has name "Carol";
$eng isa team, has name "Engineering";
insert
(member: $alice, group: $eng) isa membership;
(member: $bob, group: $eng) isa membership;
(member: $carol, group: $eng) isa membership;
`)
await insertTx.query.insert(`
match
$carol isa person, has name "Carol";
$dave isa person, has name "Dave";
$data isa team, has name "Data Science";
insert
(member: $carol, group: $data) isa membership;
(member: $dave, group: $data) isa membership;
`)
await insertTx.query.insert(`
match
$eve isa person, has name "Eve";
$frank isa person, has name "Frank";
$platform isa team, has name "Platform";
insert
(member: $eve, group: $platform) isa membership;
(member: $frank, group: $platform) isa membership;
`)
// Insert assignments (person -> project with role)
await insertTx.query.insert(`
match
$alice isa person, has name "Alice";
$api isa project, has name "API Redesign";
insert
(contributor: $alice, target: $api) isa assignment, has role-name "lead";
`)
await insertTx.query.insert(`
match
$bob isa person, has name "Bob";
$api isa project, has name "API Redesign";
insert
(contributor: $bob, target: $api) isa assignment, has role-name "developer";
`)
await insertTx.query.insert(`
match
$carol isa person, has name "Carol";
$ml isa project, has name "ML Pipeline";
insert
(contributor: $carol, target: $ml) isa assignment, has role-name "lead";
`)
await insertTx.query.insert(`
match
$dave isa person, has name "Dave";
$ml isa project, has name "ML Pipeline";
insert
(contributor: $dave, target: $ml) isa assignment, has role-name "researcher";
`)
await insertTx.query.insert(`
match
$eve isa person, has name "Eve";
$migration isa project, has name "Cloud Migration";
insert
(contributor: $eve, target: $migration) isa assignment, has role-name "lead";
`)
await insertTx.query.insert(`
match
$frank isa person, has name "Frank";
$dashboard isa project, has name "Analytics Dashboard";
insert
(contributor: $frank, target: $dashboard) isa assignment, has role-name "developer";
`)
await insertTx.commit()
console.log('Data inserted: 6 people, 3 teams, 4 projects')The match ... insert pattern references existing entities by their attributes and creates relationships in one query. No tracking internal IDs. Carol appears in both Engineering and Data Science, which is natural since a person can play the member role in multiple membership relations.
Pattern Matching Queries
TypeQL queries read like descriptions of what you're looking for. Find all members of the Engineering team:
const readTx = await dataSession.transaction(TransactionType.READ)
// Find all Engineering team members
const engMembers = readTx.query.get(`
match
$p isa person, has name $name;
$t isa team, has name "Engineering";
(member: $p, group: $t) isa membership;
get $name;
`)
console.log('\nEngineering team members:')
for await (const row of engMembers) {
const name = row.get('name')
console.log(` ${name?.value}`)
}Engineering team members:
Alice
Bob
CarolThe query declares a pattern: find a person with a name, find the Engineering team, where there's a membership connecting them. TypeDB finds all matches. No JOINs, no foreign keys, no ON clauses.
All project assignments with roles:
const assignments = readTx.query.get(`
match
$p isa person, has name $person-name;
$proj isa project, has name $project-name;
$a (contributor: $p, target: $proj) isa assignment, has role-name $role;
get $person-name, $project-name, $role;
`)
console.log('\nAll project assignments:')
for await (const row of assignments) {
const person = row.get('person-name')
const project = row.get('project-name')
const role = row.get('role')
console.log(` ${person?.value} -> ${project?.value} (${role?.value})`)
}All project assignments:
Alice -> API Redesign (lead)
Bob -> API Redesign (developer)
Carol -> ML Pipeline (lead)
Dave -> ML Pipeline (researcher)
Eve -> Cloud Migration (lead)
Frank -> Analytics Dashboard (developer)The assignment relation carries its own role-name attribute. The role belongs to the relationship, not to the person or the project. Alice isn't a "lead" in general. She's a lead on the API Redesign project.
Multi-Hop Traversals
Here's where TypeDB pulls ahead. Find all projects that Alice's teammates are working on, even if she isn't assigned to them:
const teammateProjects = readTx.query.get(`
match
$alice isa person, has name "Alice";
$team isa team;
(member: $alice, group: $team) isa membership;
$teammate isa person, has name $teammate-name;
(member: $teammate, group: $team) isa membership;
not { $teammate is $alice; };
$proj isa project, has name $project-name;
(contributor: $teammate, target: $proj) isa assignment;
get $teammate-name, $project-name;
`)
console.log("\nProjects Alice's teammates work on:")
for await (const row of teammateProjects) {
const teammate = row.get('teammate-name')
const project = row.get('project-name')
console.log(` ${teammate?.value} works on ${project?.value}`)
}
await readTx.close()Projects Alice's teammates work on:
Bob works on API Redesign
Carol works on ML PipelineOne query traversed: Alice -> her teams -> other members -> their project assignments. In SQL, that's multiple self-joins through junction tables. In TypeDB, you describe the pattern and the engine figures out the path.
not { $teammate is $alice; } excludes Alice from her own results. Negation is first-class in TypeQL.
Inference Rules
This is TypeDB's defining feature. Define logical rules in the schema, and TypeDB derives new facts at query time. You never insert them.
The rule: if person A is on the same team as person B, and B is assigned to a project, then A has indirect exposure to that project:
const ruleSession = await driver.session(DB_NAME, SessionType.SCHEMA)
const ruleTx = await ruleSession.transaction(TransactionType.WRITE)
await ruleTx.query(`
define
indirect-exposure sub relation,
relates exposed-person,
relates exposed-project;
person plays indirect-exposure:exposed-person;
project plays indirect-exposure:exposed-project;
rule teammate-project-exposure:
when {
$p1 isa person;
$p2 isa person;
not { $p1 is $p2; };
$team isa team;
(member: $p1, group: $team) isa membership;
(member: $p2, group: $team) isa membership;
$proj isa project;
(contributor: $p2, target: $proj) isa assignment;
} then {
(exposed-person: $p1, exposed-project: $proj) isa indirect-exposure;
};
`)
await ruleTx.commit()
await ruleSession.close()
console.log('\nInference rule defined: teammate-project-exposure')Query for indirect exposures. These relationships were never inserted:
const inferSession = await driver.session(DB_NAME, SessionType.DATA)
const inferTx = await inferSession.transaction(TransactionType.READ)
const exposures = inferTx.query.get(`
match
$p isa person, has name $person-name;
$proj isa project, has name $project-name;
(exposed-person: $p, exposed-project: $proj) isa indirect-exposure;
get $person-name, $project-name;
`)
console.log('\nInferred indirect project exposures:')
for await (const row of exposures) {
const person = row.get('person-name')
const project = row.get('project-name')
console.log(` ${person?.value} has exposure to ${project?.value}`)
}
await inferTx.close()
await inferSession.close()
await dataSession.close()
await driver.close()Inferred indirect project exposures:
Alice has exposure to ML Pipeline
Bob has exposure to ML Pipeline
Carol has exposure to API Redesign
Dave has exposure to API Redesign
Eve has exposure to Analytics Dashboard
Frank has exposure to Cloud MigrationNone of these rows exist in the database. TypeDB computed them: Alice is on Engineering with Carol. Carol is assigned to ML Pipeline. Therefore, Alice has indirect exposure to ML Pipeline. Same logic, every team-project combination.
This is powerful in practice. A biomedical graph: "Drug A targets Protein X, Protein X is involved in Disease Y, therefore Drug A is a candidate for Disease Y." A compliance system: "Employee A has access to System B, System B processes Data C, therefore Employee A has indirect access to Data C." Define the logic once, and it applies to all current and future data.
The SQL Equivalent
To appreciate what TypeDB just did, here's what the indirect exposure query looks like in SQL:
-- Find indirect project exposures through shared team membership
WITH teammate_projects AS (
SELECT DISTINCT
p1.name AS person_name,
proj.name AS project_name
FROM person p1
JOIN membership m1 ON m1.member_id = p1.id
JOIN membership m2 ON m2.team_id = m1.team_id
AND m2.member_id != m1.member_id
JOIN person p2 ON p2.id = m2.member_id
JOIN assignment a ON a.contributor_id = p2.id
JOIN project proj ON proj.id = a.project_id
)
SELECT person_name, project_name
FROM teammate_projects
ORDER BY person_name, project_name;Five JOINs, a CTE, and a DISTINCT. And this is a simple three-hop example. In a real knowledge graph with 10+ entity types and deep nesting, the SQL becomes unmanageable. TypeDB's pattern matching stays readable regardless of depth.
This SQL also handles exactly one traversal. Want "projects managed by someone in my department"? Write another query from scratch. In TypeDB, define another rule and query the inferred relation. The rule engine composes.
When to Use TypeDB
TypeDB is the right tool when:
- Relationships carry meaning: the connections matter as much as the things themselves. Supply chains, org hierarchies, knowledge bases.
- Type safety matters: the database must enforce that only valid relationships can exist. Biomedical data, financial compliance, regulatory systems where an invalid relationship is a liability, not just a bug.
- Inference and reasoning: you want derived facts from logical rules. Drug discovery (gene-protein-disease chains), fraud detection (transitive access patterns), impact analysis (change propagation through dependency graphs).
- Deep traversals: queries regularly span 3+ relationship hops. Social networks, recommendation engines, dependency analysis.
- Schema evolution: subtyping (a
manageris a subtype ofperson) lets schemas grow naturally as the domain evolves.
TypeDB is not the right tool for simple CRUD, high-throughput transactional workloads, or fundamentally tabular data. If your queries are mostly SELECT * FROM users WHERE id = ?, a relational database is simpler and faster.
Wrapping Up
One TypeScript file. Typed schema, interconnected data, pattern matching across multiple hops, inference rules that derive facts never explicitly stored. The same pattern scales from six people to millions of entities and relationships.
The TypeDB documentation covers advanced features like subtyping hierarchies, value constraints, negation in rules, explanation of inferred results, and clustering for production deployments.
Run the full script:
npx tsx knowledge-graph.tsTo manage your local TypeDB instance:
spindb stop typedb1 # Stop the server
spindb start typedb1 # Start it again
spindb list # See all your database instancesTypeDB handles the knowledge graph layer, but most projects also need a relational or key-value store. SpinDB manages 20+ engines, so you can run TypeDB next to PostgreSQL for transactional data or MongoDB for documents, without juggling separate installs.