Use when defining relationships between Bknd entities. Covers many-to-one, one-to-one, many-to-many, self-referencing relationships, junction tables, options like mappedBy and inversedBy, and UI vs code approaches.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Create relationships between entities in Bknd (foreign keys, references, associations).
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Create relationships between entities in Bknd (foreign keys, references, associations).
bknd-create-entity)| Type | Use Case | Example |
|---|---|---|
| Many-to-One | Child belongs to one parent | Posts → User (author) |
| One-to-One | Exclusive 1:1 pairing | User → Profile |
| Many-to-Many | Both sides have multiple | Posts ↔ Tags |
| Self-Referencing | Entity references itself | Categories → Parent Category |
npx bknd runhttp://localhost:1337posts)users)author creates author_id)Relationships are defined in the second argument to em():
const schema = em(
{
// Entity definitions (first argument)
},
({ relation, index }, entities) => {
// Relationship definitions (second argument)
}
);
Child belongs to one parent. Most common relationship type.
import { em, entity, text } from "bknd";
const schema = em(
{
users: entity("users", { email: text().required() }),
posts: entity("posts", { title: text().required() }),
},
({ relation }, { users, posts }) => {
relation(posts).manyToOne(users);
}
);
Auto-generated: users_id foreign key column on posts table
Custom field name with mappedBy:
({ relation }, { users, posts }) => {
relation(posts).manyToOne(users, {
mappedBy: "author", // Creates author_id instead of users_id
});
}
Exclusive 1:1 relationship. Each child belongs to exactly one parent.
const schema = em(
{
users: entity("users", { email: text().required() }),
profiles: entity("profiles", { bio: text() }),
},
({ relation }, { users, profiles }) => {
relation(profiles).oneToOne(users);
}
);
Note: One-to-one relationships cannot use $set operator (maintains exclusivity).
Both entities can have multiple of the other. Junction table created automatically.
const schema = em(
{
posts: entity("posts", { title: text().required() }),
tags: entity("tags", { name: text().required() }),
},
({ relation }, { posts, tags }) => {
relation(posts).manyToMany(tags);
}
);
Auto-generated: posts_tags junction table with posts_id and tags_id columns
Custom junction table name:
({ relation }, { posts, tags }) => {
relation(posts).manyToMany(tags, {
connectionTable: "post_tags", // Custom junction table name
});
}
Extra fields on junction table:
({ relation }, { users, courses }) => {
relation(users).manyToMany(courses, {
connectionTable: "enrollments",
}, {
// Extra fields on junction table
enrolled_at: date(),
completed: boolean(),
grade: number(),
});
}
Entity references itself. Common for hierarchies (categories, comments, org charts).
const schema = em(
{
categories: entity("categories", { name: text().required() }),
},
({ relation }, { categories }) => {
relation(categories).manyToOne(categories, {
mappedBy: "parent", // FK field: parent_id
inversedBy: "children", // Reverse navigation
});
}
);
Usage:
category.parent_id → Points to parent categoryapi.data.readMany("categories", { where: { parent_id: 5 } })Instead of relation(), use .references() on a number field:
const schema = em({
users: entity("users", { email: text().required() }),
posts: entity("posts", {
title: text().required(),
author_id: number().references("users.id"),
}),
});
Difference: .references() is simpler but doesn't create inverse navigation or support many-to-many.
| Option | Type | Default | Description |
|---|---|---|---|
mappedBy | string | Target entity name | FK field name (e.g., author → author_id) |
inversedBy | string | Source entity name | Reverse navigation name |
required | boolean | false | Relationship is mandatory |
| Option | Type | Default | Description |
|---|---|---|---|
connectionTable | string | {source}_{target} | Junction table name |
const api = app.getApi();
// Load posts with their author
const posts = await api.data.readMany("posts", {
with: {
users: { select: ["email", "name"] },
},
});
// Result: [{ id: 1, title: "...", users: { email: "...", name: "..." } }]
// Posts by specific author
const posts = await api.data.readMany("posts", {
where: { author_id: 5 },
});
// Using join for complex filters
const posts = await api.data.readMany("posts", {
join: {
users: { where: { email: "john@example.com" } },
},
});
// Attach tags to post
await api.data.updateOne("posts", 1, {
tags: { $attach: [1, 2, 3] }, // Tag IDs
});
// Detach tags
await api.data.updateOne("posts", 1, {
tags: { $detach: [2] },
});
// Replace all tags
await api.data.updateOne("posts", 1, {
tags: { $set: [4, 5] },
});
// Set author on post
await api.data.updateOne("posts", 1, {
users: { $set: 5 }, // User ID
});
const schema = em(
{
users: entity("users", {
email: text().required().unique(),
name: text(),
}),
posts: entity("posts", {
title: text().required(),
content: text(),
published: boolean(),
}),
tags: entity("tags", {
name: text().required().unique(),
}),
},
({ relation }, { users, posts, tags }) => {
// Post has one author
relation(posts).manyToOne(users, { mappedBy: "author" });
// Posts have many tags
relation(posts).manyToMany(tags);
}
);
const schema = em(
{
customers: entity("customers", { email: text().required() }),
orders: entity("orders", { total: number() }),
products: entity("products", { name: text().required(), price: number() }),
},
({ relation }, { customers, orders, products }) => {
// Order belongs to customer
relation(orders).manyToOne(customers);
// Order has many products (with quantity)
relation(orders).manyToMany(products, {
connectionTable: "order_items",
}, {
quantity: number().required(),
unit_price: number().required(),
});
}
);
const schema = em(
{
categories: entity("categories", {
name: text().required(),
slug: text().required().unique(),
}),
},
({ relation }, { categories }) => {
relation(categories).manyToOne(categories, {
mappedBy: "parent",
inversedBy: "children",
});
}
);
// Usage: Get all children of category 5
const children = await api.data.readMany("categories", {
where: { parent_id: 5 },
});
Error: Entity "user" not found
Fix: Entity names are plural by convention. Use users not user.
// Wrong
relation(posts).manyToOne(user);
// Correct
relation(posts).manyToOne(users);
Error: Circular dependency detected
Fix: For self-referencing, use proper options:
// Correct self-reference
relation(categories).manyToOne(categories, {
mappedBy: "parent",
inversedBy: "children",
});
Error: Field "users_id" already exists
Fix: Use mappedBy to specify a different field name:
// If you already have users_id, use a different name
relation(posts).manyToOne(users, { mappedBy: "author" }); // Creates author_id
Error: Cannot use $set on one-to-one relation
Fix: One-to-one maintains exclusivity differently. Use $create instead:
// For one-to-one
await api.data.updateOne("users", 1, {
profiles: { $create: { bio: "Hello" } },
});
Error: Cannot read property 'manyToOne' of undefined
Fix: Ensure entity is destructured from second callback parameter:
// Wrong - missing users in destructure
({ relation }, { posts }) => {
relation(posts).manyToOne(users); // users is undefined
}
// Correct
({ relation }, { users, posts }) => {
relation(posts).manyToOne(users);
}
Problem: Added relation but not seeing FK column.
Fixes:
em() argumentnpx bknd debug paths
# Look for the FK field in entity output
const api = app.getApi();
// Create parent
const user = await api.data.createOne("users", { email: "test@example.com" });
// Create child with relation
const post = await api.data.createOne("posts", {
title: "Test Post",
author_id: user.data.id,
});
// Load with relation
const loaded = await api.data.readOne("posts", post.data.id, {
with: { users: true },
});
console.log(loaded.data.users); // { id: 1, email: "test@example.com" }
DO:
users, posts)mappedBy for semantic field names (author instead of users)em() argument.references() for simple FK without navigationDON'T:
relation() (it creates them automatically)$set on one-to-one relations.references() for simple FKswith and join$attach, $detach, $set for relation updates