The starting point

Think in Items, not tables.

Fjell gives you a common model for a graph of objects that are related to one another. Start with a real domain: a User, the Company they work for, their Addresses, and the Teams where they contribute.

One User. A connected domain.

Our example User is not the whole application. They are one node in a useful graph. A User has a first name, last name, and perhaps a birth date. Their work points to a Company. Their addresses belong to them. Their team memberships carry roles such as manager or member.

User · PItem

Identity and profile

u-42 identifies the User independently. The User can be loaded without first loading an Address, Company, or Team.

Company · PItem

Where they work

A User can reference a Company as another primary Item. Both objects have their own identity and lifecycle.

Address · CItem

Where they live

A User can have many Addresses. Their location in the graph is expressed through the User parent, not by flattening everything into User.

Team membership · CItem

How they contribute

A Team can have many memberships. Each membership can point to a User and carry a role: manager, member, or another application-defined role.

The graph should be impossible to miss.

The arrows describe domain relationships. The labels describe how those relationships can be represented in Fjell.

Acme's people graphExample domain · not a schema prescription
PItem · User
Alex MorganfirstName · lastName · birthDate
worksAt Company
PItem · Company
Acme SystemscompanyId: acme-7
employees Users
many CItems · Address
Home + mailingstreet · city · postalCode
User → addresses[] child locations
PItem · Team
Platform teamTeam memberships carry roles
memberships[] manager · member

Relationships carry meaning.

User → Company is a PItem-to-PItem reference. The User and Company stand on their own, and the relationship can be represented by a key or domain property such as companyId.

User → Addresses is a PItem-to-many-CItem relationship. An Address is still a full Item, but its location is reached through its User parent.

Team → memberships is a useful child collection. A membership can reference a User and add relationship data that does not belong on either endpoint:

Team:       team-9
Membership: alex-morgan
role:       "manager"
joinedAt:   "2025-04-12"

A reciprocal view is possible too: “Which Teams is Alex on?” and “Who reports to Alex on this Team?” are graph queries over related Items. The model does not require one giant User record containing every object.

PItem vs CItem

The distinction is about how an Item is reached through the hierarchy. It is not a distinction between important and unimportant objects.

PItem

Primary Item

A top-level Item addressed independently, such as /users/:id, /companies/:id, or /teams/:id.

CItem

Child Item

An Item reached through a parent location, such as /users/:userId/addresses/:id or /teams/:teamId/memberships/:id.

const alex = await users.operations.get({ kt: "user", pk: "u-42" });
const home = await alex.addresses.operations.get({ kt: "address", pk: "home" });
const team = await teams.operations.get({ kt: "team", pk: "team-9" });

Why the libraries are separate

Once the graph has a shared shape, each package owns one part of its journey: types define the language, validation protects boundaries, libraries apply operations and business rules, storage adapters persist Items, routers transport them, clients consume them, and cache/providers keep them useful in applications.

Follow one Item end to end →Browse the layers →