.agents/skills/bknd-define-relationship/SKILL.md
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 skillsauth add cameronapak/freedom-stack-v3 bknd-define-relationshipInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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: "[email protected]" } },
},
});
// 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: "[email protected]" });
// 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: "[email protected]" }
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 updatesdevelopment
Use btca (Better Context App) to efficiently query and learn from the bknd backend framework. Use when working with bknd for (1) Understanding data module and schema definitions, (2) Implementing authentication and authorization, (3) Setting up media file handling, (4) Configuring adapters (Node, Cloudflare, etc.), (5) Learning from bknd source code and examples, (6) Debugging bknd-specific issues
development
Use when configuring webhook integrations in Bknd. Covers receiving incoming webhooks via HTTP triggers, sending outgoing webhooks with FetchTask, event-triggered webhooks on data changes, signature verification, retry patterns, and async processing.
development
Use when encountering Bknd errors, getting error messages, something not working, or needing quick fixes. Covers error code reference, quick solutions, and common mistake patterns.
tools
Use when writing tests for Bknd applications, setting up test infrastructure, creating unit/integration tests, or testing API endpoints. Covers in-memory database setup, test helpers, mocking, and test patterns.