npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kengo

v0.1.6

Published

A Prisma-like, type-safe, and reactive ORM for IndexedDB. Say goodbye to callbacks and enjoy a modern developer experience in the browser

Readme

⚔︎

K E N G O

剣豪

A modern, type-safe, and reactive ORM for IndexedDB
Wield the power of a server-side ORM, directly in the browser

NPM Version Bundle Size Tests License
TypeScript IndexedDB Zero Deps Browser


Managing data in the browser shouldn't feel like a clumsy battle. IndexedDB is powerful, but its native API is a maze of callbacks and verbose boilerplate—like wielding a blunt, heavy sword in the fog.

Kengo brings the art of the swordsman—mastery, precision, and elegance—to this battlefield. Inspired by the masterful developer experience of Prisma, Kengo allows you to command your browser's database with the confidence and clarity of a Kengo (a master swordsman).


The Way of Kengo - 剣豪之道

  • ⚔️ Schema First (The Form - 型): Define your data structure in a single, readable schema file. This is your kata—the source of truth from which all data interactions flow with discipline and purpose.
  • Fully Type-Safe (The Blade - 刃): Wield an auto-generated, fully-typed client. Your editor becomes your whetstone, sharpening your code and eliminating errors at compile time for a flawless edge.
  • 🌊 Fluent & Familiar API (The Stance - 構え): Enjoy a modern, chainable API that feels just like Prisma. Every query is an elegant, powerful stance, allowing you to strike with precision and grace.
  • 🚀 Reactive & Real-time (The Mind - 心): Go beyond simple CRUD. With liveQuery, your UI can subscribe to data changes and update automatically. Your application becomes a living entity, aware and responsive, like a swordsman in a state of total concentration.
  • 🛡️ Zero-Config Migrations (The Scabbard - 鞘): Evolve your schema without fear. Simply increment your version number, and Kengo handles the migration automatically. Your blade is always ready, seamlessly adapting to new challenges.

Quick Start: The First Cut

Experience the power of Kengo in minutes.

# npm
npm install kengo

# yarn
yarn add kengo

# pnpm
pnpm add kengo

# bun
bun add kengo

Define your schema, forge your client, and wield the blade.

import { defineSchema, Kengo } from 'kengo'

// 1. Define your schema (The Form)
const schema = defineSchema({
  version: 1,
  stores: {
    users: {
      '@@id': { keyPath: 'id', autoIncrement: true },
      '@@uniqueIndexes': ['email'],
      '@@indexes': ['age'],
    },
  },
})

// 2. Forge your client
const db = new Kengo({ name: 'my-app-db', schema })

// 3. Wield the blade with a familiar, type-safe API!
async function main() {
  const newUser = await db.users.create({
    data: { name: 'Musashi', email: '[email protected]', age: 29 },
  })

  const skilledSwordsmen = await db.users.findMany({
    where: { age: { gte: 25 } },
    orderBy: { name: 'asc' },
    take: 10,
  })

  console.log('Found:', skilledSwordsmen)
}

main()

Comparison: Kengo vs. The Alternatives

| Feature | Kengo (The Master's Blade) | Native IndexedDB (Raw Iron) | localStorage (Swiss Army Knife) | | -------------------- | -------------------------------------- | ----------------------------------- | ------------------------------- | | API | ✅ Async/Await, Fluent, Prisma-like | ❌ Callback-based, verbose, complex | ❌ Synchronous, blocking, basic | | Type Safety | ✅ Fully Type-Safe, Auto-generated | ❌ any, error-prone | ❌ String-only, manual parsing | | Querying | ✅ Powerful, indexed, multi-faceted | ❌ Manual cursors & ranges | ❌ Key-only, no indexing | | Migrations | ✅ Automatic, safe, version-controlled | ❌ Manual, high-risk, boilerplate | ❌ N/A | | Storage Capacity | ✅ Very Large (GBs+) | ✅ Very Large (GBs+) | ❌ Tiny (~5MB) | | Reactivity | ✅ Built-in liveQuery | ❌ None, requires manual polling | ❌ storage event (limited) |

The Roadmap: The Path to Mastery

Kengo is an actively developed project on a mission to deliver the ultimate browser database experience. Here's what the future holds:

  • [x] Core: Rock-solid, type-safe CRUD & Query Engine.
  • [x] Migrations: Automatic schema migrations.
  • [ ] Reactivity: liveQuery API for real-time UI updates.
  • [ ] Relations: Simple, Prisma-like relation queries (include, select).
  • [ ] Full-Text Search: Integrated full-text search capabilities.
  • [ ] Data Seeding: A dedicated API for seeding development data.
  • [ ] Kengo Studio: A developer tool for visualizing and managing your local database.

📚 Complete API Reference

🏗️ Schema Definition

Define your database structure with a type-safe schema.

const schema = defineSchema({
  version: 1, // Increment to trigger migrations
  stores: {
    users: {
      '@@id': { keyPath: 'id', autoIncrement: true }, // Primary key
      '@@indexes': ['email', 'age', 'country'], // Queryable indexes
      '@@uniqueIndexes': ['email'], // Unique constraints
    },
  },
})

Schema Options:

  • version: Database version (positive integer)
  • stores: Object defining your tables/stores
  • @@id: Primary key configuration
    • String: '@@id': 'userId' (simple key path)
    • Object: { keyPath: 'id', autoIncrement: true } (auto-increment)
  • @@indexes: Array of field names for non-unique indexes
  • @@uniqueIndexes: Array of field names for unique indexes

✨ CRUD Operations

Create Operations

// Create single record
const user = await db.users.create({
  data: { name: 'Alice', email: '[email protected]', age: 25 },
  select: { id: true, name: true }, // Optional: Return specific fields
})

// Create multiple records
const result = await db.users.createMany({
  data: [
    { name: 'Bob', email: '[email protected]', age: 30 },
    { name: 'Charlie', email: '[email protected]', age: 35 },
  ],
  skipDuplicates: true, // Skip records that violate unique constraints
})
// Returns: { count: 2 }

Read Operations

// Find by unique field (returns null if not found)
const user = await db.users.findUnique({
  where: { id: 1 }, // or { email: '[email protected]' }
  select: { name: true, email: true },
})

// Find first matching record (returns null if not found)
const firstUser = await db.users.findFirst({
  where: { age: { gte: 25 } },
  orderBy: { createdAt: 'desc' },
})

// Find multiple records
const users = await db.users.findMany({
  where: {
    age: { gte: 18, lte: 65 },
    country: 'US',
  },
  orderBy: [{ age: 'desc' }, { name: 'asc' }],
  skip: 10,
  take: 20,
  select: { id: true, name: true, email: true },
})

// Count records
const count = await db.users.count({
  where: { age: { gte: 18 } },
})

Update Operations

// Update single record
const updated = await db.users.update({
  where: { id: 1 },
  data: {
    name: 'Alice Smith',
    age: { increment: 1 }, // Atomic operation
  },
  select: { id: true, name: true, age: true },
})

// Update multiple records
const result = await db.users.updateMany({
  where: { country: 'US' },
  data: {
    isActive: true,
    credits: { multiply: 1.1 }, // Give 10% bonus
  },
})
// Returns: { count: 42 }

Delete Operations

// Delete single record (throws if not found)
const deleted = await db.users.delete({
  where: { id: 1 },
})

// Delete multiple records
const result = await db.users.deleteMany({
  where: { age: { lt: 18 } },
})
// Returns: { count: 5 }

Upsert Operation

// Create if not exists, update if exists
const user = await db.users.upsert({
  where: { email: '[email protected]' },
  create: {
    name: 'Alice',
    email: '[email protected]',
    age: 25,
  },
  update: {
    age: { increment: 1 },
    lastSeen: new Date(),
  },
  select: { id: true, name: true, age: true },
})

🔍 Query Conditions

Where Conditions

// Equality (implicit)
where: { name: 'Alice' }

// Equality (explicit)
where: { name: { equals: 'Alice' } }

// Not equal
where: { status: { not: 'deleted' } }

// In array
where: { role: { in: ['admin', 'moderator'] } }

// Not in array
where: { status: { notIn: ['deleted', 'suspended'] } }

// Greater than / Greater than or equal
where: { age: { gt: 18 } }
where: { age: { gte: 18 } }

// Less than / Less than or equal
where: { price: { lt: 100 } }
where: { price: { lte: 100 } }

// String operations
where: { email: { contains: '@gmail.com' } }
where: { name: { startsWith: 'John' } }
where: { url: { endsWith: '.com' } }

// Combine multiple conditions (AND)
where: {
  age: { gte: 18, lte: 65 },
  country: 'US',
  isActive: true
}

Ordering

// Single field
orderBy: {
  createdAt: 'desc'
}

// Multiple fields
orderBy: [{ category: 'asc' }, { price: 'desc' }]

Pagination

// Skip and take
{
  skip: 20,   // Skip first 20 records
  take: 10    // Take next 10 records
}

Field Selection

// Select specific fields
select: {
  id: true,
  name: true,
  email: true
  // Other fields will not be returned
}

⚛️ Atomic Operations

Perform atomic numeric operations without race conditions.

// Increment
await db.counters.update({
  where: { id: 1 },
  data: { value: { increment: 5 } },
})

// Decrement
await db.counters.update({
  where: { id: 1 },
  data: { value: { decrement: 3 } },
})

// Multiply
await db.counters.update({
  where: { id: 1 },
  data: { value: { multiply: 2 } },
})

// Divide (ignores division by zero)
await db.counters.update({
  where: { id: 1 },
  data: { value: { divide: 4 } },
})

// Combine with regular updates
await db.users.update({
  where: { id: 1 },
  data: {
    name: 'Updated Name',
    points: { increment: 100 },
    multiplier: { multiply: 1.5 },
  },
})

🔄 Transactions

Ensure data consistency with ACID transactions.

// Basic transaction
const result = await db.$transaction(async (tx) => {
  // All operations use 'tx' instead of 'db'
  const user = await tx.users.create({
    data: { name: 'Alice', email: '[email protected]' },
  })

  const post = await tx.posts.create({
    data: {
      userId: user.id,
      title: 'First Post',
      content: 'Hello World!',
    },
  })

  await tx.users.update({
    where: { id: user.id },
    data: { postCount: { increment: 1 } },
  })

  return { user, post } // Return value from transaction
})

// Transaction with error handling
try {
  await db.$transaction(async (tx) => {
    await tx.accounts.update({
      where: { id: senderId },
      data: { balance: { decrement: amount } },
    })

    if (amount > 1000) {
      throw new Error('Amount too large!') // Rollback
    }

    await tx.accounts.update({
      where: { id: receiverId },
      data: { balance: { increment: amount } },
    })
  })
} catch (error) {
  // Transaction rolled back, no changes applied
  console.error('Transaction failed:', error)
}

Transaction Rules:

  • All operations within a transaction are atomic
  • If any operation fails, all changes are rolled back
  • Nested transactions are not supported
  • Cannot call $disconnect() within a transaction
  • Can access raw database with tx.$getRawDB()

🔄 Automatic Migrations

Kengo handles schema migrations automatically when you increment the version number.

// Version 1: Initial schema
const schemaV1 = defineSchema({
  version: 1,
  stores: {
    users: {
      '@@id': { keyPath: 'id', autoIncrement: true },
      '@@indexes': ['email'],
    },
  },
})

// Version 2: Add new store and indexes
const schemaV2 = defineSchema({
  version: 2, // Increment version to trigger migration
  stores: {
    users: {
      '@@id': { keyPath: 'id', autoIncrement: true },
      '@@indexes': ['email', 'createdAt'], // Added new index
      '@@uniqueIndexes': ['username'], // Added unique constraint
    },
    posts: {
      // New store added
      '@@id': { keyPath: 'id', autoIncrement: true },
      '@@indexes': ['userId', 'publishedAt'],
    },
  },
})

// Kengo automatically:
// 1. Detects version change (1 → 2)
// 2. Creates new stores (posts)
// 3. Adds new indexes (createdAt, username)
// 4. Preserves all existing data
// 5. Handles the migration safely
const db = new Kengo({
  name: 'my-app',
  schema: schemaV2, // Just use the new schema!
})

Migration Features:

  • Zero-config: Just increment the version number
  • Non-destructive: Existing data is always preserved
  • Additive changes: Add new stores, indexes, and unique constraints
  • Automatic handling: No migration files or scripts needed
  • Safe rollback: Old app versions continue to work with their schema version

Important Notes:

  • Always increment the version number when changing schema
  • You cannot remove stores or indexes (IndexedDB limitation)
  • Schema changes are applied when the database is first opened
  • Each browser profile maintains its own schema version

🔧 Advanced Features

Raw Database Access

// Get the underlying IDBDatabase instance
const rawDB = await db.$getRawDB()

// Use native IndexedDB API for advanced operations
const transaction = rawDB.transaction(['users'], 'readonly')
const objectStore = transaction.objectStore('users')
const index = objectStore.index('email')
// ... perform native IndexedDB operations

Connection Management

// Initialize database (called automatically on first operation)
await db.$connect()

// Close database connection
await db.$disconnect()

// Check connection status
const isConnected = db.$isConnected()

📝 TypeScript Support

Kengo provides full TypeScript support with auto-generated types.

import { defineSchema, Kengo } from 'kengo'

// Define your data types
interface User {
  id?: number
  name: string
  email: string
  age: number
  createdAt: Date
}

// Schema is fully typed
const schema = defineSchema({
  version: 1,
  stores: {
    users: {
      '@@id': { keyPath: 'id', autoIncrement: true },
      '@@uniqueIndexes': ['email'],
    },
  },
})

// Client is fully typed based on schema
const db = new Kengo({ name: 'my-app', schema })

// All operations are type-safe
const user = await db.users.create({
  data: {
    name: 'Alice', // ✅ Required
    email: '[email protected]', // ✅ Required
    age: 25, // ✅ Required
    // id is optional (auto-increment)
    // unknown: 'field'  // ❌ TypeScript error
  },
})

🚀 Performance Tips

  1. Use Indexes: Define indexes for fields you query frequently
  2. Select Fields: Use select to return only needed fields
  3. Batch Operations: Use createMany, updateMany, deleteMany for bulk operations
  4. Transactions: Group related operations in transactions
  5. Pagination: Use skip and take for large datasets

Contributing

Kengo's path to mastery is forged by its community. Contributions of all forms are welcome, from bug reports to new features. Please see our Contributing Guide to get started.

License

Licensed under the MIT License.