IndexedDB ORM

TypeScript ORM wrapper for IndexedDB with Zod runtime validation, built on top of Dexie.js

GitHub NPM Contact MIT License

Installation

npm install @indexeddb-orm/idb-orm

Features

TypeScript Support

Full type safety with generics and IntelliSense support. Write type-safe database operations with complete IDE assistance.

Zod Validation

Runtime validation with Zod schemas. Ensure data integrity with powerful validation rules and clear error messages.

Config-based Entities

Define entities with a simple `defineEntity()` - clean, functional approach to entity definition.

Dexie Compatibility

All Dexie.js operations supported. Leverage the full power of Dexie with additional ORM capabilities.

Clean API

Simple and intuitive API design. Focus on your business logic, not database complexity.

Relations & Aggregations

Support for entity relationships and powerful aggregation operations. Build complex queries with ease.

Quick Start

1. Define Entities

import { BaseEntity, defineEntity } from '@indexeddb-orm/idb-orm'; import { z } from 'zod'; const UserSchema = z.object({ id: z.number().optional(), name: z.string().min(2), email: z.string().email(), age: z.number().min(18) }); export class UserEntity extends BaseEntity { name!: string; email!: string; age!: number; } defineEntity(UserEntity, { tableName: 'users', schema: UserSchema, columns: { name: { required: true, indexed: true }, email: { required: true, unique: true }, age: { indexed: true } } });

2. Create Database

import { Database } from '@indexeddb-orm/idb-orm'; export const db = Database.createDatabase({ name: 'MyApp', version: 1, entities: [UserEntity] });

3. Use in Your App

// Create and save const user = new UserEntity(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; await db.getRepository(UserEntity).add(user); // Query with full type safety const users = await db.getRepository(UserEntity) .where('age') .above(18) .toArray();

Example Applications

Comprehensive demo applications showcasing all features across different frameworks:

React Demo App

React 19 + TypeScript
Material-UI, Live queries, Cloud sync, Aggregations
cd react-demo-app && npm install && npm run dev
View Source

Vue Demo App

Vue 3 + TypeScript
Vuetify 3, Reactive components, Entity management
cd vue-demo-app && npm install && npm run dev
View Source

Svelte Demo App

SvelteKit + TypeScript
Material-UI, Server-side rendering, Live queries
cd svelte-demo-app && npm install && npm run dev
View Source

Angular Demo App

Angular 17 + TypeScript
Angular Material, Services, Dependency injection
cd angular-demo-app && npm install && npm start
View Source

Complete Manual

Notes

⚠️ Dexie Dependencies

Important: This library includes Dexie.js internally. To ensure that liveQuery works properly, you need to configure Vite to dedupe Dexie.

// In your vite.config.ts, add:
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    dedupe: ['dexie'],
  },
  // ... other config
});

// This ensures liveQuery works correctly
// Without dedupe, liveQuery may not function properly

🏗️ Built on Dexie.js

Foundation: This library is built on top of Dexie.js - a powerful wrapper for IndexedDB. All Dexie.js methods and features are available through the db object.

// All Dexie.js methods work with our library
const db = await Database.createDatabase({...});

// Direct Dexie.js operations
await db.transaction('rw', db.users, async () => {
    await db.users.add({ name: 'John', email: 'john@example.com' });
});

// Dexie.js query methods
const users = await db.users
    .where('age')
    .above(18)
    .and(user => user.name.startsWith('J'))
    .toArray();

// Dexie.js liveQuery (reactive queries)
import { liveQuery } from 'dexie';
const observableUsers = liveQuery(() => 
    db.users.where('active').equals(true).toArray()
);

Benefits: You get all the power of Dexie.js plus our ORM features like entity validation, relations, and cloud sync.

🔧 Cloud Sync Setup Steps

To enable cloud synchronization, you need to complete several setup steps:

// 1. Install Dexie Cloud addon
npm install dexie-cloud-addon

// 2. Configure your database with cloud sync
const db = await Database.createDatabase({
    name: 'MyAppDB',
    version: 1,
    entities: [User, Post],
    cloudSync: {
        appId: 'your-app-id',
        serverUrl: 'https://your-server.com',
        // ... other cloud config
    }
});

// 3. Enable cloud sync after database creation
await db.enableCloudSync();

📊 Index Requirements for Queries

Critical: When searching by any field, you MUST create an index on that field. Queries without proper indexes will fail.

const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string(), age: z.number() }); defineEntity(User, { schema: UserSchema, indexes: [ { key: 'email' }, // Required for email queries { key: 'age' }, // Required for age queries { key: 'name' } // Required for name queries ] });

🔄 Schema Changes & Migrations

When changing entity schemas, you need to handle migrations properly. The library provides two strategies for schema changes.

// Strategy 1: Selective reset (recommended)
const db = await Database.createDatabase({
    name: 'MyAppDB',
    version: 2, // Increment version
    entities: [User, Post],
    onSchemaChangeStrategy: 'selective' // Only affected tables are reset
});

// Strategy 2: Full reset (use with caution)
onSchemaChangeStrategy: 'all' // All data is lost

🔍 Compound Indexes for Complex Queries

For queries involving multiple fields, you need compound indexes. Single field indexes won't work for multi-field queries.

defineEntity(Post, { schema: PostSchema, compoundIndexes: [ { key: ['category', 'status'] }, { key: ['authorId', 'createdAt'] } ] });

Usage

1. Define Entities

import { BaseEntity, defineEntity } from '@indexeddb-orm/idb-orm'; import { z } from 'zod'; // Zod schema for validation const UserSchema = z.object({ id: z.number().optional(), name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Invalid email format'), age: z.number().min(18, 'Must be at least 18 years old'), createdAt: z.number(), updatedAt: z.number(), isActive: z.boolean().default(true), tags: z.array(z.string()).default([]), metadata: z.record(z.string(), z.unknown()).default({}), }); export class UserEntity extends BaseEntity { name!: string; email!: string; age!: number; createdAt!: number; updatedAt!: number; isActive!: boolean; tags!: string[]; metadata!: Record; } defineEntity(UserEntity, { tableName: 'users', schema: UserSchema, columns: { name: { required: true, indexed: true }, email: { required: true, unique: true }, age: { indexed: true }, createdAt: { indexed: true }, updatedAt: { indexed: true }, isActive: { indexed: true }, tags: {}, metadata: {}, }, });

2. Create Database

import { Database } from '@indexeddb-orm/idb-orm'; import { UserEntity } from './entities/User'; import { PostEntity } from './entities/Post'; export const db = Database.createDatabase({ name: 'MyApp', version: 1, entities: [UserEntity, PostEntity], config: { onSchemaChangeStrategy: 'all', cloudSync: { databaseUrl: 'https://your-sync-server.com', enableOfflineSupport: true, syncInterval: 30000 } }, });

3. Basic CRUD Operations

// Create new entity with validation in one call import { newEntity } from '@indexeddb-orm/idb-orm'; const newUser = newEntity(UserEntity, { name: 'John Doe', email: 'john@example.com', age: 25, createdAt: Date.now(), updatedAt: Date.now(), tags: ['developer'], metadata: { source: 'manual' }, }); // Save to database await db.getRepository(UserEntity).add(newUser); // Query with full Dexie.js API const activeUsers = await db.getRepository(UserEntity) .where('isActive') .equals(true) .toArray();

4. Entity Relations

// Define entities with relations defineEntity(UserEntity, { tableName: 'users', schema: UserSchema, columns: { /* ... */ }, relations: { posts: { type: 'one-to-many', target: PostEntity, foreignKey: 'authorId' }, profile: { type: 'one-to-one', target: ProfileEntity, foreignKey: 'userId' } } }); // Load relations const userWithPosts = await db.loadRelations({ entity: user, entityClass: UserEntity, relationNames: ['posts', 'profile'] }); // Load specific relation const userPosts = await db.loadRelationByName({ entity: user, entityClass: UserEntity, relationName: 'posts' });

5. Reactive Queries with liveQuery

Use Dexie's liveQuery for reactive data that automatically updates when the database changes. Works with all major frameworks:

React Example
import { liveQuery } from 'dexie';
import { useObservable } from 'dexie-react-hooks';
import { Database } from '@indexeddb-orm/idb-orm';
import { User } from './entities/User';

function UserList() {
  const users = useObservable(
    liveQuery(() => {
      const userRepo = db.getRepository(User);
      return userRepo.where('active').equals(true).toArray();
    })
  );
  
  return (
    
{users?.map(user => (
{user.name}
))}
); }
Vue Example
import { liveQuery } from 'dexie';
import { ref, onMounted, onUnmounted } from 'vue';
import { Database } from '@indexeddb-orm/idb-orm';
import { User } from './entities/User';

export default {
  setup() {
    const users = ref([]);
    let subscription;
    
    onMounted(() => {
      subscription = liveQuery(() => {
        const userRepo = db.getRepository(User);
        return userRepo.where('active').equals(true).toArray();
      }).subscribe(result => {
        users.value = result;
      });
    });
    
    onUnmounted(() => {
      subscription?.unsubscribe();
    });
    
    return { users };
  }
};
Svelte Example
import { liveQuery } from 'dexie';
import { onMount, onDestroy } from 'svelte';
import { Database } from '@indexeddb-orm/idb-orm';
import { User } from './entities/User';

let users: User[] = [];
let subscription;

onMount(() => {
  subscription = liveQuery(() => {
    const userRepo = db.getRepository(User);
    return userRepo.where('active').equals(true).toArray();
  }).subscribe(result => {
    users = result;
  });
});

onDestroy(() => {
  subscription?.unsubscribe();
});
Angular Example
import { liveQuery } from 'dexie';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Database } from '@indexeddb-orm/idb-orm';
import { User } from './entities/User';

@Component({
  selector: 'app-user-list',
  template: '
{{user.name}}
' }) export class UserListComponent implements OnInit, OnDestroy { users: User[] = []; private subscription?: Subscription; ngOnInit() { this.subscription = liveQuery(() => { const userRepo = db.getRepository(User); return userRepo.where('active').equals(true).toArray(); }).subscribe(result => { this.users = result; }); } ngOnDestroy() { this.subscription?.unsubscribe(); } }

6. Advanced Queries and Aggregation

// Aggregation operations const result = await db.aggregate({ entityClass: PostEntity, options: { where: { category: 'tech' }, sort: { field: 'views', direction: 'desc' }, limit: 10, count: true, sum: ['views'], avg: ['likes'], groupBy: ['category'] } }); // Complex filtering and sorting const topPosts = await db.getRepository(PostEntity) .where('published') .equals(true) .and(post => post.views > 100) .orderBy('views') .reverse() .limit(5) .toArray();

6. Entity Validation

// Manual validation const user = new UserEntity(); user.name = 'A'; // Too short user.email = 'invalid-email'; // Invalid format const result = user.validate(); if (!result.isValid) { console.log(result.errors); // ['name: Name must be at least 2 characters', 'email: Invalid email format'] } // Validation with error throwing try { user.validateOrThrow(); } catch (error) { console.log('Validation failed:', error.message); } // Initialize with validation const validUser = new UserEntity().init({ name: 'John Doe', email: 'john@example.com', age: 25 });

7. Database Management

// Check for schema changes const hasChanges = await db.checkSchemaChanges(); if (hasChanges) { await db.performSelectiveReset(); // Reset only changed tables // or await db.resetDatabase(); // Reset entire database } // Clear all data await db.clearAllData(); // Run migrations await db.runMigrations([ { version: 2, name: 'Add user profiles', up: async (db) => { // Migration logic for version 2 } } ]);

8. Cloud Synchronization

// Enable cloud sync await db.enableCloudSync({ databaseUrl: 'https://your-sync-server.com', enableOfflineSupport: true, syncInterval: 30000 }); // Manual sync await db.sync(); // Check sync status const status = db.getSyncStatus(); console.log('Sync enabled:', status.enabled); console.log('Last sync:', status.lastSync); // Sync specific tables await db.syncTables(['users', 'posts']); // Disable cloud sync db.disableCloudSync();

9. Compound Indexes

// Define entity with compound indexes defineEntity(UserEntity, { tableName: 'users', schema: UserSchema, columns: { name: { indexed: true }, email: { indexed: true, unique: true }, age: { indexed: true }, isActive: { indexed: true }, }, compoundIndexes: [ { columns: ['name', 'email'], unique: false, name: 'name_email_index' }, { columns: ['age', 'isActive'], unique: false, name: 'age_active_index' } ] }); // Query using compound index const users = await db.getRepository(UserEntity) .where(['name', 'email']) .equals(['John Doe', 'john@example.com']) .toArray(); const activeAdults = await db.getRepository(UserEntity) .where(['age', 'isActive']) .above([18, 1]) .toArray();

Complete API Reference

Complete reference of all methods with JSDoc documentation from the library.

Core Classes

Database

Main database class extending Dexie with ORM capabilities.

Static Methods:
Database.createDatabase(params)
Create database with entity registration
const db = Database.createDatabase({ name: 'app-db', version: 1, entities: [User, Post] });
Instance Methods:
getRepository<T>(entityClass)
Get repository for entity (TypeORM style)
const users = db.getRepository(User); const id = await users.add({ name: 'Ann' } as User);
aggregate<T>(params)
Perform aggregation on entity data
const result = await db.aggregate({ entityClass: Post, options: { where: { category: 'tech' } } });
clearAllData()
Clear all data from database
await db.clearAllData();
resetDatabase()
Reset database when schema changes
await db.resetDatabase();
checkSchemaChanges()
Check if schema has changed and reset if needed
const changed = await db.checkSchemaChanges();
performSelectiveReset()
Manually perform selective reset for changed tables only
await db.performSelectiveReset();
runMigrations(migrations)
Run migrations for schema changes
await db.runMigrations(migrations);
getTypedTable<T>(entityClass)
Get typed table for entity
const posts = db.getTypedTable(Post);
getTableForEntity<T>(entityClass)
Get table with proper typing for specific entity
const users = db.getTableForEntity(User);
getEntities()
Get all entities
const all = db.getEntities();
getEntity(tableName)
Get entity by table name
const UserClass = db.getEntity('users');
loadRelations<T>(params)
Load relations for an entity
const userWithRelations = await db.loadRelations({ entity: user, entityClass: User, relationNames: ['posts'] });
loadRelationByName<T, K>(params)
Load a specific relation by name
const posts = await db.loadRelationByName({ entity: { id: userId }, entityClass: User, relationName: 'posts' });
saveWithRelations<T>(params)
Save entity with relations
const saved = await db.saveWithRelations({ entity: user, entityClass: User });
deleteWithRelations<T>(params)
Delete entity with cascade handling for relations
await db.deleteWithRelations({ entity: user, entityClass: User });
sync()
Manual sync with cloud
getSyncStatus()
Get sync status
enableCloudSync(config)
Enable cloud sync (if not already enabled)
disableCloudSync()
Disable cloud sync
isCloudSyncEnabled()
Check if cloud sync is enabled
getCloudSyncConfig()
Get cloud sync configuration
syncTables(tableNames)
Force sync specific tables

BaseEntity<TKey>

Base class for all entities with validation capabilities.

validate(): ValidationResult
Validate entity data against its schema
const result = user.validate(); if (!result.isValid) console.error(result.errors);
validateOrThrow(): void
Validate entity data and throw error if invalid
user.validateOrThrow(); // throws ValidationError on invalid data
init(data): this
Initialize entity with data and validate
const user = new User().init({ name: 'Alice' });

EntitySchema<T>

Schema management for entities.

getTableName(): string
Get the table name for this entity
const schema = new EntitySchema(User); const table = schema.getTableName(); // 'users'
getSchema(): ZodSchema | undefined
Get the Zod schema for this entity
const schema = new EntitySchema(User, userZodSchema); const zod = schema.getSchema();
getColumns(): Record<string, unknown>
Get column metadata for this entity
const cols = new EntitySchema(User).getColumns(); // cols.name.required === true
validate(data): ValidationResult
Validate data against entity schema
const { isValid, errors } = new EntitySchema(User, userSchema).validate({ name: 'Alice' });
create(data?): T
Create a new instance of the entity
const user = new EntitySchema(User).create({ name: 'Bob' });

AggregationService

Service for performing aggregation operations on entity data.

aggregate<T>(entityClass, options)
Perform aggregation operations on entity data
const result = await aggregationService.aggregate(Post, { where: { category: 'tech' }, sort: { field: 'views', direction: 'desc' }, limit: 10, count: true, sum: ['views'] });
loadRelation(entity, relationMeta)
Load a specific relation based on metadata
const related = await (aggregationService as any).loadRelation(record, { type: 'one-to-many', target: Post, foreignKey: 'authorId' });

RelationLoader

Service for loading and saving entity relations.

loadRelations<T>(entity, entityClass, relationNames?)
Load relations for an entity
loadRelationByName<T, K>(entity, entityClass, relationName)
Load a specific relation by name
loadRelation<T>(entity, relationMeta)
Load a specific relation based on metadata
saveWithRelations<T>(entity, entityClass)
Save entity with all its relations
deleteWithRelations<T>(entity, entityClass)
Delete entity with cascade handling for relations

SchemaBuilder

Service for building IndexedDB schemas from entities.

buildSchema(entities)
Build indexeddb schema from entities
const schema = SchemaBuilder.buildSchema([User, Post]);
buildTableSchemas(entities)
Build detailed table schemas for comparison
const tables = SchemaBuilder.buildTableSchemas([User, Post]);
generateSchemaHash(schema)
Generate hash for schema to detect changes
const hash = SchemaBuilder.generateSchemaHash(schema);
compareSchemas(oldSchemas, newSchemas)
Compare two schema arrays and return changes
const changes = SchemaBuilder.compareSchemas(oldSchemas, newSchemas);
storeTableSchemas(dbName, tableSchemas)
Store table schemas in localStorage
SchemaBuilder.storeTableSchemas('app-db', tables);
getStoredTableSchemas(dbName)
Get stored table schemas from localStorage
const tables = SchemaBuilder.getStoredTableSchemas('app-db');

CloudSyncService

Service for cloud synchronization functionalities.

initializeCloudSync(config)
Initialize cloud synchronization with configuration
await cloudService.initializeCloudSync({ databaseUrl: 'https://example.cloud', enableOfflineSupport: true, syncInterval: 30000 });
stopPeriodicSync()
Stop periodic synchronization
cloudService.stopPeriodicSync();
sync()
Perform manual synchronization
await cloudService.sync();
getSyncStatus()
Get current synchronization status
const { enabled, lastSync, isOnline } = cloudService.getSyncStatus();
enableCloudSync(config)
Enable cloud synchronization
await cloudService.enableCloudSync({ databaseUrl: 'https://example.cloud' });
disableCloudSync()
Disable cloud synchronization
cloudService.disableCloudSync();
isCloudSyncEnabled()
Check if cloud sync is currently enabled
if (cloudService.isCloudSyncEnabled()) { // ... }
getCloudSyncConfig()
Get current cloud sync configuration
const cfg = cloudService.getCloudSyncConfig();
syncTables(tableNames)
Synchronize specific tables
await cloudService.syncTables(['users', 'posts']);

MigrationManager

Service for managing database migrations.

runMigrations(migrations, db)
Run database migrations
performSelectiveReset(entities, db)
Perform selective reset for changed tables only
resetDatabase(db)
Reset entire database by clearing all data
checkSchemaChanges(entities, db)
Check if schema has changed and reset database if needed

Entity Definition

defineEntity(EntityClass, options)

Define entity metadata without decorators. Registers table name, schema, columns, indexes and relations for a given class so the ORM can work without TypeScript decorators.

Parameters:
  • EntityClass - The entity constructor/class
  • options - Metadata describing table, schema, columns, indexes and relations
Options:
interface DefineEntityOptions { tableName?: string; // Custom table name schema?: ZodSchema; // Zod validation schema timestamps?: boolean; // Enable timestamps columns?: Record<string, ColumnConfig>; relations?: Record<string, RelationConfig>; compoundIndexes?: { name?: string; unique?: boolean; columns: string[] }[]; }
Example:
defineEntity(UserEntity, { tableName: 'users', columns: { name: { required: true, indexed: true }, email: { required: true, unique: true }, }, relations: { posts: { type: 'one-to-many', target: PostEntity, foreignKey: 'authorId' } } });

newEntity(EntityClass, data)

Create a new entity instance and validate it in a single call. This helper instantiates the provided entity class, assigns the given partial data, and immediately validates it using the entity's `init` method (which calls `validateOrThrow` under the hood).

Parameters:
  • EntityClass - Entity constructor (must have a zero-arg constructor)
  • data - Partial entity data to assign before validation
Returns:

A fully initialized and validated entity instance

Throws:

ValidationError if validation fails according to the entity schema

Example:
const user = newEntity(User, { name: 'Alice' }); // user is validated; throws if required fields are missing or invalid

Type Definitions

EntityConstructor<T>

interface EntityConstructor<T extends BaseEntity = BaseEntity> { new (): T; schema?: ZodSchema; tableName?: string; }

ColumnConfig

type ColumnConfig = { required?: boolean; unique?: boolean; indexed?: boolean; default?: unknown | (() => unknown); };

RelationConfig

type RelationConfig = { type: 'one-to-one' | 'one-to-many' | 'many-to-many'; target: ClassConstructor | string; foreignKey?: string; joinTable?: string; cascade?: boolean; eager?: boolean; };

ValidationResult

interface ValidationResult { isValid: boolean; errors: string[]; }

DatabaseConfig

interface DatabaseConfig { name: string; version: number; entities: EntityConstructor[]; onSchemaChangeStrategy?: 'selective' | 'all'; migrations?: Migration[]; cloudSync?: CloudSyncConfig; }

CloudSyncConfig

interface CloudSyncConfig { databaseUrl: string; enableOfflineSupport?: boolean; syncInterval?: number; }

Migration

interface Migration { version: number; name: string; up: (db: Dexie) => Promise<void>; down?: (db: Dexie) => Promise<void>; }

AggregationOptions<T>

interface AggregationOptions<T extends BaseEntity> { where?: Partial<T>; count?: boolean; sum?: (keyof T)[]; avg?: (keyof T)[]; min?: (keyof T)[]; max?: (keyof T)[]; groupBy?: keyof T; include?: string[]; limit?: number; sort?: { field: keyof T; direction: 'asc' | 'desc'; }; }

AggregationResult

interface AggregationResult { count?: number; sum?: Record<string, number>; avg?: Record<string, number>; min?: Record<string, number>; max?: Record<string, number>; groups?: Array<{ key: unknown; count: number; sum?: Record<string, number>; avg?: Record<string, number>; min?: Record<string, number>; max?: Record<string, number>; }>; data?: unknown[]; }

Error Classes

ValidationError

Error thrown when entity validation fails.

Properties:
  • message: string - Error message
  • errors: string[] - Array of validation errors

Schema Change Strategies

'selective'

Only resets tables that have schema changes, preserving data in unchanged tables.

'all'

Resets the entire database when any schema change is detected.

Cloud Sync Status

interface SyncStatus { enabled: boolean; lastSync?: Date; isOnline?: boolean; }

Get Started Today

Ready to build powerful IndexedDB applications with full TypeScript support?

Created by Maciej Radom | MIT License