askill
data-schema

data-schemaSafety 90Repository

Use when defining Bknd entities, fields, relationships, and indices. Covers entity definition with em(), field types (text, number, boolean, date, enumm, json, jsonschema), field configuration (required, default_value, validation), relationship types (many-to-one, one-to-one, many-to-many, polymorphic), and index creation (simple, composite, unique).

0 stars
1.2k downloads
Updated 2/5/2026

Package Files

Loading files...
SKILL.md

Data Schema

Define your data model with entities, fields, and relationships. Bknd uses a declarative API similar to Drizzle/Prisma but with fewer boilerplate.

What You'll Learn

  • Define entities with em() and entity()
  • Use all field types with proper configuration
  • Create relationships between entities
  • Add indices for performance
  • Follow naming conventions and best practices

Entity Definition

Basic Entity

Use em() to define multiple entities:

import { em, entity, text, boolean } from "bknd";

const schema = em({
  todos: entity("todos", {
    title: text().required(),
    done: boolean(),
  }),
});

Auto-generated fields:

  • id (primary key) - automatically added to every entity
  • Format depends on database: SQLite uses integer, PostgreSQL uses UUID

Multiple Entities

const schema = em({
  users: entity("users", {
    email: text().required(),
    name: text(),
  }),
  posts: entity("posts", {
    title: text().required(),
    content: text(),
  }),
});

Field Types

Primary Field

Primary key is automatically added to every entity:

entity("users", {
  email: text().required(),
})
// Auto-generated: id (integer or UUID)

Customize primary key format:

entity("users", {
  email: text().required(),
}, {
  primary_format: "uuid",  // "integer" | "uuid" (default: "integer")
})

Note: Use the timestamps plugin for automatic created_at/updated_at fields.

Enum Field

import { enumm } from "bknd";

entity("users", {
  role: enumm({
    enum: ["admin", "user", "guest"],
  }).required(),
  status: enumm({
    enum: {
      ACTIVE: "active",
      INACTIVE: "inactive",
      PENDING: "pending",
    },
    default_value: "PENDING",
  }),
})

JSON Field

import { json } from "bknd";

entity("users", {
  metadata: json<{ theme: string; notifications: boolean }>(),
  preferences: json<string[]>({
    default_value: [],
  }),
})

JSON Schema Field

For validated JSON with schema enforcement:

import { jsonschema } from "bknd";

entity("events", {
  payload: jsonschema({
    type: "object",
    properties: {
      type: { type: "string" },
      data: { type: "object" },
    },
    required: ["type", "data"],
  }),
})

Relationships

Many-to-One

Posts belong to one user:

const schema = em(
  {
    users: entity("users", {
      email: text().required(),
    }),
    posts: entity("posts", {
      title: text().required(),
    }),
  },
  ({ relation }, { users, posts }) => {
    relation(posts).manyToOne(users);
  }
);

Auto-generated: user_id foreign key on posts table

One-to-One

User has one settings profile:

const schema = em(
  {
    users: entity("users", {
      email: text().required(),
    }),
    settings: entity("settings", {
      theme: text(),
    }),
  },
  ({ relation }, { users, settings }) => {
    relation(settings).oneToOne(users);
  }
);

Note: One-to-one cannot use $set operator for updates (maintains exclusivity).

Many-to-Many

Posts have many tags:

const schema = em(
  {
    posts: entity("posts", {
      title: text().required(),
    }),
    tags: entity("tags", {
      name: text().required(),
    }),
  },
  ({ relation }, { posts, tags }) => {
    relation(posts).manyToMany(tags);
  }
);

Auto-generated: Junction table posts_tags with post_id and tag_id

Many-to-Many with Custom Fields

const schema = em(
  {
    users: entity("users", {
      email: text().required(),
    }),
    courses: entity("courses", {
      title: text().required(),
    }),
  },
  ({ relation }, { users, courses }) => {
    relation(users).manyToMany(courses, {
      connectionTable: "user_courses",
    }, {
      enrolled_at: date(),
      completed_at: date(),
    });
  }
);

Self-Referencing

Categories can have parent/child relationships:

const schema = em(
  {
    categories: entity("categories", {
      name: text().required(),
    }),
  },
  ({ relation }, { categories }) => {
    relation(categories).manyToOne(categories, {
      inversedBy: "children",
      mappedBy: "parent",
    });
  }
);

Indices

Simple Index

const schema = em(
  {
    users: entity("users", {
      email: text().required(),
    }),
  },
  ({ index }, { users }) => {
    index(users).on(["email"]);
  }
);

Unique Index

const schema = em(
  {
    users: entity("users", {
      email: text().required(),
      username: text().required(),
    }),
  },
  ({ index }, { users }) => {
    index(users).on(["email"], true); // Second parameter = unique
  }
);

Composite Index

const schema = em(
  {
    posts: entity("posts", {
      author_id: number(),
      published: boolean(),
      created_at: date(),
    }),
  },
  ({ index }, { posts }) => {
    index(posts).on(["author_id", "published"]);
  }
);

Chained Index Definition

const schema = em(
  {
    users: entity("users", {
      email: text().required(),
      username: text().required(),
    }),
  },
  ({ index }, { users }) => {
    index(users)
      .on(["email"], true)  // Unique index on email
      .on(["username"], true) // Unique index on username
      .on(["created_at"]);  // Non-unique index
  }
);

Field Validation

Built-in Validation

Fields validate data on insert/update:

entity("users", {
  age: number({
    minimum: 18,
    maximum: 120,
  }),
  email: text({
    pattern: "^[^@]+@[^@]+\\.[^@]+$", // Basic email regex
  }),
})

Validation Errors

For server-side mutations, use the API:

const result = await api.data.createOne("users", {
  email: "invalid-email", // Fails pattern validation
  age: 15,               // Fails minimum validation
});

if (result.error) {
  console.log(result.error); // Validation error details
}

Naming Conventions

Entity Names

Use plural, lowercase entity names:

// ✅ Good
entity("users", { ... })
entity("posts", { ... })

// ❌ Bad
entity("User", { ... })
entity("user", { ... })

Field Names

Use snake_case for database compatibility:

// ✅ Good
entity("users", {
  first_name: text(),
  last_name: text(),
  created_at: date(),
})

// ❌ Bad (works but inconsistent)
entity("users", {
  firstName: text(),  // Stored as firstName in DB
  lastName: text(),
})

Type Generation

Extract types from schema for type safety:

const schema = em({
  users: entity("users", {
    email: text().required(),
    name: text(),
  }),
});

type Database = (typeof schema)["DB"];
declare module "bknd" {
  interface DB extends Database {}
}

Generate with CLI:

npx bknd types

DOs and DON'Ts

DO:

  • Use plural entity names (users, posts, comments)
  • Use snake_case for field names (created_at, user_id)
  • Add unique indices for fields used in lookups (email, username)
  • Use composite indices for frequently filtered combinations
  • Generate types with npx bknd types for full type safety
  • Use timestamps plugin for created_at/updated_at fields
  • Index foreign key fields (user_id, post_id)

DON'T:

  • Define the same entity twice in em()
  • Forget .required() for non-nullable fields
  • Skip indices on frequently queried fields (you'll see warnings)
  • Use PascalCase for entity or field names
  • Store large binary data in JSON fields (use media fields instead)
  • Overuse jsonschema unless you need runtime validation

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/10/2026

A comprehensive and high-quality technical reference for defining data schemas in the Bknd framework. It provides clear, actionable code examples for entities, fields, relationships, and indices, along with best practices.

90
100
85
95
98

Metadata

Licenseunknown
Version-
Updated2/5/2026
Publishermajiayu000

Tags

apidatabase