Skip to content

Relations

Relations define how collections are connected at the database level. They enable Rebase to:

  • Render relation picker fields in entity forms
  • Resolve related entities when displaying previews
  • Generate foreign key constraints in the Drizzle schema
  • Support cascade delete/update behaviors

Relations can be defined either inline within the property, or explicitly in the relations array of a collection:

You can define the relation directly on the property. The framework automatically extracts these into the collection’s relations[] at normalization time, so you no longer need a separate relations[] entry for properties.

const postsCollection: EntityCollection = {
slug: "posts",
name: "Posts",
table: "posts",
properties: {
title: { type: "string", name: "Title" },
content: { type: "string", name: "Content", multiline: true },
author: {
type: "relation",
name: "Author",
target: () => usersCollection,
cardinality: "one",
direction: "owning",
localKey: "author_id"
}
}
};

For advanced use cases or when a relation doesn’t map directly to a form field, you can define it in the relations array:

const postsCollection: EntityCollection = {
slug: "posts",
name: "Posts",
table: "posts",
properties: {
title: { type: "string", name: "Title" },
content: { type: "string", name: "Content", multiline: true },
author: { type: "relation", name: "Author", relationName: "author" }
},
relations: [
{
relationName: "author",
target: () => usersCollection,
cardinality: "one",
localKey: "author_id"
}
]
};

A foreign key on this table points to another table’s primary key.

relations: [
{
relationName: "author",
target: () => usersCollection,
cardinality: "one", // This entity has ONE author
direction: "owning", // The FK is on THIS table
localKey: "author_id" // Column on the posts table
}
]

This creates: posts.author_id → users.id

The foreign key is on the target table, pointing back to this entity.

// On the Users collection:
relations: [
{
relationName: "posts",
target: () => postsCollection,
cardinality: "many", // This user has MANY posts
direction: "inverse", // The FK is on the TARGET table
foreignKeyOnTarget: "author_id" // Column on the posts table
}
]

Two collections connected through an intermediate junction table.

// On the Users collection:
relations: [
{
relationName: "roles",
target: () => rolesCollection,
cardinality: "many",
direction: "owning",
through: {
table: "user_roles", // Junction table name
sourceColumn: "user_id", // FK to this collection
targetColumn: "role_id" // FK to target collection
}
}
]

This creates:

CREATE TABLE user_roles (
user_id INTEGER REFERENCES users(id),
role_id INTEGER REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);

To render a relation field in a form, add a property with type: "relation":

properties: {
author: {
type: "relation",
name: "Author",
target: () => usersCollection, // Target collection
widget: "select" // "select" (dropdown) or "dialog" (full picker)
}
}

Relation field in form

When rendering a preview (like in a table cell or a reference chip), Rebase handles hydration automatically:

Relation preview in table

For complex relationships that traverse multiple tables, use joinPath:

// Users → Permissions through Roles
relations: [
{
relationName: "permissions",
target: () => permissionsCollection,
cardinality: "many",
joinPath: [
{
table: "user_roles",
on: { from: "id", to: "user_id" }
},
{
table: "roles",
on: { from: "role_id", to: "id" }
},
{
table: "role_permissions",
on: { from: "id", to: "role_id" }
},
{
table: "permissions",
on: { from: "permission_id", to: "id" }
}
]
}
]
joinPath: [
{
table: "customers",
on: {
from: ["company_code", "region_id"], // Multiple columns
to: ["code", "region_id"]
}
}
]

Control what happens when related entities are updated or deleted:

relations: [
{
relationName: "author",
target: () => usersCollection,
cardinality: "one",
localKey: "author_id",
onDelete: "cascade", // Delete posts when user is deleted
onUpdate: "cascade" // Update FK when user ID changes
}
]
ActionBehavior
"cascade"Propagate the change to related rows
"restrict"Prevent the operation if related rows exist
"no action"Same as restrict (defer to constraint check)
"set null"Set the FK column to NULL
"set default"Set the FK column to its default value

When querying data through the Rebase Client SDK, relations are not included by default. Use the include() method to request related entities alongside the primary data.

const { data } = await client.data.articles
.include("author", "categories")
.find();
const { data } = await client.data.articles
.include("*")
.find();
const { data } = await client.data.articles.find({
include: ["author", "categories"]
});

When included, the response contains both the scalar foreign key and the hydrated relation object:

const { data } = await client.data.articles
.include("author")
.find();
for (const article of data) {
// Scalar FK — always present
article.values.author_id; // "uuid-1234"
// Hydrated relation — only present when included
article.values.author?.name; // "Jane Doe"
}

The relation names passed to include() must match the relationName defined in the collection’s relations array.

For the full query builder reference (filtering, sorting, pagination, real-time), see the Client SDK documentation.

interface Relation {
relationName?: string;
target: () => EntityCollection;
cardinality: "one" | "many";
direction?: "owning" | "inverse";
inverseRelationName?: string;
localKey?: string;
foreignKeyOnTarget?: string;
through?: {
table: string;
sourceColumn: string;
targetColumn: string;
};
joinPath?: JoinStep[];
onUpdate?: "cascade" | "restrict" | "no action" | "set null" | "set default";
onDelete?: "cascade" | "restrict" | "no action" | "set null" | "set default";
overrides?: Partial<EntityCollection>;
validation?: { required?: boolean };
}