Getting Started with TypeDB
TypeDB is a strongly typed database for data where the relationships matter as much as the records. You declare entity types, relation types, and attribute types in a schema, and the database enforces that structure on every read and write. Its query language, TypeQL, matches patterns across arbitrarily deep relationships without the join chains that shape would cost you in SQL. If you are evaluating it: this post walks the whole thing end to end, and when to use TypeDB near the bottom is the honest answer on where it does and does not pay off.
The problem it exists for: most databases store flat records. Tables (or collections), rows, queries that match conditions. That 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 models that directly. The schema is built on type theory, so the database enforces logical constraints at every level rather than leaving them to application code, and you can define functions that compute derived facts from the data you already have. Alice's indirect exposure becomes something you query for, not something you insert.
This post targets TypeDB 3.x and TypeQL 3, which is a real break from the 2.x tutorials you'll find floating around. If you learned TypeDB a couple of years ago, the two biggest changes to know up front: sessions are gone (you open transactions directly now), and the old rule blocks were replaced by functions. I'll flag the differences as we hit them. Everything below is run against TypeDB 3.12.
We'll build a knowledge graph for a software company from schema to derived facts, all in the TypeDB console so the TypeQL is front and center. Run it locally with the Layerbase CLI or against a managed instance on Layerbase Cloud.
Contents
- Create a TypeDB Instance
- Open the Console and Create a Database
- Define the Schema
- Insert the Data
- Pattern Matching Queries
- Multi-Hop Traversals
- Functions That Derive Facts
- Fetch JSON Instead of Rows
- The SQL Equivalent
- Use It From Your App
- When to Use TypeDB
- Wrapping Up
Create a TypeDB Instance
Local with the Layerbase CLI
The Layerbase CLI (formerly SpinDB) gets TypeDB running locally without Docker or manual binary management. (What is the Layerbase CLI?)
Install the Layerbase CLI globally:
npm i -g layerbase # npm
pnpm add -g layerbase # pnpmCreate and start a TypeDB instance:
lbase create typedb1 -e typedb --startThe CLI downloads the TypeDB 3.x binary for your platform, initializes it, and starts the server. TypeDB 3.x requires authentication, and the CLI sets up the default admin / password credentials for you.
Layerbase Cloud
Rather skip the local install? Layerbase Cloud runs managed TypeDB instances, and TypeDB is on the free plan. Pick TypeDB from the engine list and grab the host, port, username, and password from the Quick Connect panel.
The only difference for a cloud instance is the address and TLS. Everything after "create a database" in this guide is identical whether you're local or on Layerbase. I'll show both connection commands in the next section.
Open the Console and Create a Database
TypeDB ships a console binary that speaks TypeQL directly. The Layerbase CLI drops you into it:
lbase connect typedb1Connecting to a Layerbase Cloud instance is the same console, pointed at your host. Cloud instances use TLS, so you do not pass --tls-disabled. Leave --password off and the console prompts for it, which keeps the password out of your shell history and out of the process list:
typedb console --address your-host.cloud.layerbase.dev:1729 \
--username adminA TypeDB server hosts multiple databases, each with its own schema and data. Create one:
database create companyEverything else runs inside a transaction. TypeDB 3.x has three transaction types: schema for type definitions, write for data, and read for queries. You open one, run your queries, then commit (schema and write) or close (read). If you used TypeDB 2.x, this is where sessions used to live. They're gone. You open the transaction against the database directly.
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.
Open a schema transaction and paste the definition:
transaction schema company
define
attribute name, value string;
attribute email, value string;
attribute department, value string;
attribute status, value string;
attribute role-name, value string;
entity person,
owns name,
owns email,
plays membership:member,
plays assignment:contributor;
entity team,
owns name,
owns department,
plays membership:group;
entity project,
owns name,
owns status,
plays assignment:target;
relation membership,
relates member,
relates group;
relation assignment,
relates contributor,
relates target,
owns role-name;
commitAlready 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 (a person) to a group (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 carry properties. Relational databases handle this with an extra column on a junction table, and property graphs handle it inconsistently.
Insert the Data
Open a write transaction and insert six people, three teams, and four projects:
transaction write company
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
$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
$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";Now wire people to teams. The match ... insert pattern finds existing entities by their attributes and creates relations between them:
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;
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;
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;Carol appears in both Engineering and Data Science, which is fine. A person can play the member role in more than one membership.
Then assign people to projects, with the role carried on the relation:
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";
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";
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";
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";
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";
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";Commit the whole batch:
commitNo tracking of internal IDs anywhere. You reference entities by their attributes, and the engine finds them.
Pattern Matching Queries
TypeQL queries read like descriptions of what you're looking for. Open a read transaction and find every member of the Engineering team. The select clause picks which variables come back:
transaction read company
match
$p isa person, has name $name;
$t isa team, has name "Engineering";
(member: $p, group: $t) isa membership;
select $name;The console prints one row per answer:
-----------
$name | isa name "Alice"
-----------
$name | isa name "Bob"
-----------
$name | isa name "Carol"
-----------
Finished. Total answers: 3Each cell shows the concept it matched: an attribute of type name with a value. The query declares a pattern (a person with a name, the Engineering team, a membership connecting them) and TypeDB finds every match. No JOINs, no foreign keys, no ON clauses.
Every project assignment with its role. You can bind a relation to a variable with links when you want to attach attributes to it:
match
$p isa person, has name $person-name;
$proj isa project, has name $project-name;
$a isa assignment, links (contributor: $p, target: $proj), has role-name $role;
select $person-name, $project-name, $role; -------------------
$person-name | isa name "Alice"
$project-name | isa name "API Redesign"
$role | isa role-name "lead"
-------------------
$person-name | isa name "Bob"
$project-name | isa name "API Redesign"
$role | isa role-name "developer"
-------------------
$person-name | isa name "Carol"
$project-name | isa name "ML Pipeline"
$role | isa role-name "lead"
-------------------
... (Dave, Eve, Frank follow)
Finished. Total answers: 6The role-name 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 every project that Alice's teammates work on, even the ones she isn't assigned to:
match
$alice isa person, has name "Alice";
$team isa team;
(member: $alice, group: $team) isa membership;
(member: $teammate, group: $team) isa membership;
not { $teammate is $alice; };
$teammate has name $teammate-name;
(contributor: $teammate, target: $proj) isa assignment;
$proj has name $project-name;
select $teammate-name, $project-name; --------------------
$project-name | isa name "API Redesign"
$teammate-name | isa name "Bob"
--------------------
$project-name | isa name "ML Pipeline"
$teammate-name | isa name "Carol"
--------------------
Finished. Total answers: 2One query traversed Alice to her teams to the other members to their assignments. In SQL, that's multiple self-joins through junction tables. In TypeQL, you describe the shape and the engine figures out the path.
The not { $teammate is $alice; } block excludes Alice from her own results. Negation is first-class in TypeQL.
Functions That Derive Facts
This is where TypeDB 3.x parts ways with the tutorials you may have seen. TypeDB 2.x had rule blocks that materialized inferred facts. In 3.x those are gone, replaced by functions: reusable, parameterized queries you define in the schema and call from other queries. They're a cleaner fit, they compose, and you can pass arguments.
Here's the same "indirect exposure" idea as a function. If a person shares a team with someone assigned to a project, that person has indirect exposure to it. The function takes a person and returns a stream of project:
transaction schema company
define
fun teammate-exposure($p: person) -> { project }:
match
$team isa team;
(member: $p, group: $team) isa membership;
(member: $teammate, group: $team) isa membership;
not { $teammate is $p; };
(contributor: $teammate, target: $proj) isa assignment;
return { $proj };
commitNow call it. The let $proj in teammate-exposure($p) line streams every project the function returns for each person:
transaction read company
match
$p isa person, has name $person-name;
let $proj in teammate-exposure($p);
$proj has name $project-name;
select $person-name, $project-name; -------------------
$person-name | isa name "Alice"
$project-name | isa name "API Redesign"
-------------------
$person-name | isa name "Alice"
$project-name | isa name "ML Pipeline"
-------------------
$person-name | isa name "Bob"
$project-name | isa name "API Redesign"
-------------------
... (Bob/ML, Carol/API, Carol/ML, Dave/ML, Eve/Analytics, Frank/Cloud)
Finished. Total answers: 9None of these pairs were inserted. Alice is on Engineering with Bob (API Redesign) and Carol (ML Pipeline), so she has exposure to both. Carol sits on two teams, so she picks up her Engineering teammates' work and her Data Science teammate's work. Define the logic once and it applies to every person, current and future.
This pattern travels. A biomedical graph: "Drug A targets Protein X, Protein X is involved in Disease Y, so Drug A is a candidate for Disease Y." A compliance system: "Employee A can access System B, System B processes Data C, so Employee A has indirect access to Data C." A function encodes the rule once and every query that calls it stays a one-liner.
Fetch JSON Instead of Rows
select returns concept rows, which is what you want in the console. When you're feeding an application, fetch builds JSON documents in whatever shape you ask for:
match
$p isa person, has name $name, has email $email;
fetch {
"name": $name,
"email": $email
};{
"email": "alice@example.com",
"name": "Alice"
}
{
"email": "bob@example.com",
"name": "Bob"
}
... (one document per person)The SQL Equivalent
To appreciate what the traversal did, here's the indirect-exposure query in SQL:
-- Find indirect project exposures through shared team membership
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
ORDER BY person_name, project_name;Five JOINs and a DISTINCT, for a three-hop example. In a real graph with a dozen entity types and deeper nesting, the SQL gets unmanageable. The TypeQL function stays readable regardless of depth, and it composes: want "projects managed by someone in my department" next? Write another function and call it. The SQL is a rewrite from scratch every time.
Use It From Your App
The console is great for exploring, but you'll want a driver for application code. TypeDB 3.x drivers speak the gRPC protocol on port 1729, which is exactly what Layerbase Cloud exposes. The maintained 3.x drivers are Python, Rust, and Java. Here's the Python driver running the Engineering-members query:
from typedb.driver import (
TypeDB,
Credentials,
DriverOptions,
DriverTlsConfig,
TransactionType,
)
# Local Layerbase CLI instance: TLS is off. On Layerbase Cloud use
# DriverTlsConfig.enabled_with_native_root_ca() and your cloud host.
with TypeDB.driver(
"localhost:1729",
Credentials("admin", "password"),
DriverOptions(DriverTlsConfig.disabled()),
) as driver:
with driver.transaction("company", TransactionType.READ) as tx:
answers = tx.query(
'match $p isa person, has name $n; '
'$t isa team, has name "Engineering"; '
"(member: $p, group: $t) isa membership; select $n;"
).resolve()
for row in answers:
print(row.get("n").get_string())Alice
Bob
CarolOne note if you're a Node shop: the npm typedb-driver package is still 2.x and won't talk to a 3.x server. There's a newer TypeScript driver built on TypeDB's HTTP endpoint, but Layerbase doesn't expose that endpoint today, so on Layerbase reach for the console, one of the gRPC drivers above, or the query console built into the dashboard. If you specifically need the HTTP driver or TypeDB Studio, that's what TypeDB Cloud is for.
When to Use TypeDB
TypeDB earns its place when:
- Relationships carry meaning. The connections matter as much as the things. Supply chains, org hierarchies, knowledge bases.
- The database must enforce validity. Only sane relationships can exist. Biomedical data, financial compliance, regulatory systems where an invalid relationship is a liability, not just a bug.
- You want derived facts. Functions compute drug-gene-disease chains, transitive access, or change propagation on demand instead of you hand-writing each traversal.
- Queries span several hops. Social graphs, recommendations, dependency analysis.
It's not the tool for simple CRUD, high-throughput transactional workloads, or plainly tabular data. If your queries are mostly SELECT * FROM users WHERE id = ?, a relational database is simpler and faster.
Wrapping Up
One console session took us from an empty database to a typed schema, interconnected data, pattern matching across multiple hops, and a function that derives facts nobody inserted. The same TypeQL scales from six people to millions of entities.
The TypeDB documentation goes deeper on subtyping, value constraints, negation, and functions. The 2.x to 3.x reference is worth a read if you're carrying old TypeQL forward.
Manage your local instance with the Layerbase CLI:
lbase stop typedb1 # Stop the server
lbase start typedb1 # Start it again
lbase list # See all your database instancesTypeDB handles the knowledge-graph layer, but most projects also need a relational or key-value store. The Layerbase CLI manages 20+ engines, so you can run TypeDB next to PostgreSQL for transactional data or Valkey for caching without juggling separate installs. When you're ready to host it, TypeDB is on the Layerbase Cloud free plan.
Keep reading
- What TypeDB's Type System Actually Buys YouTypeDB is not a property graph with extra syntax. Entities, relations with named roles, and attributes are three independent kinds of type, and TypeDB 3 replaced inference rules with typed functions. Here is what that changes about how you model.
- TypeDB 3.12 is Now on LayerbaseTypeDB 3.12 landed this week with a given stage for injection-safe batch queries, @doc and @meta schema annotations, a new bulk loader, and an admin service that is off by default. You can spin one up free on Layerbase in seconds.
- KuzuDB alternatives: a managed graph database without the embedded ceilingKuzu (KuzuDB) is a fast embedded graph database, but with no server and no managed hosting. When to move to a managed graph database, and how TypeDB fits.
- From PGlite to Production PostgresPGlite is a real Postgres compiled to WASM, so graduating a prototype to a hosted database is a dump and a restore, not a rewrite. Here is the whole path, start to finish.