Skip to content

Getting Started with TypeDB

9 min readTypeDBDatabasesKnowledge Graph

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

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:

bash
npm i -g layerbase    # npm
pnpm add -g layerbase # pnpm

Create and start a TypeDB instance:

bash
lbase create typedb1 -e typedb --start

The 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:

bash
lbase connect typedb1

Connecting 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:

bash
typedb console --address your-host.cloud.layerbase.dev:1729 \
  --username admin

A TypeDB server hosts multiple databases, each with its own schema and data. Create one:

text
database create company

Everything 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:

typeql
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;

commit

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 (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:

typeql
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:

typeql
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:

typeql
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:

text
commit

No 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:

typeql
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:

text
   -----------
    $name | isa name "Alice"
   -----------
    $name | isa name "Bob"
   -----------
    $name | isa name "Carol"
   -----------
Finished. Total answers: 3

Each 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:

typeql
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;
text
   -------------------
    $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: 6

The 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:

typeql
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;
text
   --------------------
    $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: 2

One 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:

typeql
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 };

commit

Now call it. The let $proj in teammate-exposure($p) line streams every project the function returns for each person:

typeql
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;
text
   -------------------
    $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: 9

None 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:

typeql
match
  $p isa person, has name $name, has email $email;
fetch {
  "name": $name,
  "email": $email
};
text
{
  "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:

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:

python
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())
text
Alice
Bob
Carol

One 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:

bash
lbase stop typedb1    # Stop the server
lbase start typedb1   # Start it again
lbase list            # See all your database instances

TypeDB 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.