A high-performance file-based local database for JavaScript/TypeScript. Leverages classes for models, allowing for methods and virtual properties while maintaining ease of setup.
- Fully type-safe via TypeScript + Zod
- Strong runtime validation
- Class-based models with methods & virtuals
- Concurrency-safe (async-mutex + p-limit)
- Atomic file writes (temp + rename)
- File-backed, no server or DB setup
- Perfect for CLIs, bots, and tools
bun add @neisanworks/neisandb
# or
npm install @neisanworks/neisandb
# or
pnpm add @neisanworks/neisandb// src/lib/server/database/models/user.ts
import { type Data, Model } from '@neisanworks/neisandb';
import * as z from 'zod/v4';
export const UserSchema = z.object({
email: z.email(),
password: z
.string()
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/),
attempts: z.number().min(0).default(0)
});
export type UserSchema = typeof UserSchema;
export class UserModel extends Model<UserSchema> {
email!: string;
password!: string;
attempts: number = 0
constructor(data: Data, id: bigint) {
super(UserSchema, id)
this.hydrate(data)
}
get locked(): boolean {
return this.attempts >= 3;
}
authenticate(password: string): boolean {
return password === this.password;
}
}// src/lib/server/database/index.ts
import { Database } from '@neisanworks/neisandb'
import { UserModel, UserSchema } from './models/user.ts'
const db = new Database({
folder: "./data", // optional
concurrency: 25 // optional
})
export const Users = db.collection({
name: 'users',
model: UserModel,
schema: UserSchema,
uniques: ['email'] // optional
})// src/lib/remote/auth.remote.ts
import { error } from '@sveltejs/kit';
import { form } from '$app/server';
import { Users } from '$lib/server/database';
import { LoginSchema } from '$lib/utils/schemas/auth';
export const login = form(LoginSchema, async ({ email, password }) => {
const user = await Users.findOne((user) => user.email === email);
if (!user) {
error(401, "Invalid Email/Password");
};
if (!user.authenticate(password)) {
const update = await Users.findOneAndUpdate(user.id, (user) => {
user.attempts++;
});
if (update.success && update.data.locked) {
error(423, "Account Locked; Contact System Admin");
};
error(401, "Invalid Email/Password");
};
return { message: "Authenticated" };
})- Schema: Define shape and validation using
zod/v4 - Model: Extends
Modelto use methods and virtual properties - Collection:
db.collection({ name, model, schema })defines a persistent collection - Persistence: Each collection is backed by its own
.nsdbfile - Validation: Validation occurs during record creation and each model property update
Each collection is stored in its own .nsdb file under your folder path
neisandb
├── data
│ └── users.tmp # Temporary file cteated during atomic file writing
│ └── users.nsdb # Users collection file
├── models
│ └── user.ts # Users collection model
└── index.ts # Database initialization and collection exporting (optional; collection can be created and exported anywhere)
neisandb supports a range of methods for querying, updating, and mutating
insert: creates a new record and returns it
const user = await Users.insert({ email: 'email@email.com', password: '$omePassw0rd' });
// Returns { success: false, errors: Record<keyof Schema, string> } | { success: true, data: UserModel }findOne: returns one model from the database
// ID Query, using a bigint
const user = await Users.findOne(0n);
// or, Predicate Search, returning the first to match
const user = await Users.findOne((record, id) => record.email === email);
// Returns UserModel | undefined if not foundfind: returns multiple models from the database
// No Parameter, returning all models from the database
const users = await Users.find();
// or, Predicate Search, returning all matching models from the database
const users = await Users.find((record, id) => id > 10n);
// Pagination
const users = await Users.find({ offset: 5, limit: 10 });
// or
const users = await Users.find((record, id) => record.attempts < 3, {
offset: 5,
limit: 10,
});
// Returns Array<UserModel> | undefined if none are found or pagination is out of boundsfindOneAndUpdate: finds one record, update it, and return its model
// ID Query, using a bigint
const update = await Users.findOneAndUpdate(0n, (user) => {
user.email = 'newemail@email.com'; // validation occurs at the property-level in its setter-method
});
// or, Predicate Search, updating the first match
const update = await Users.findOneAndUpdate(
(record, id) => record.email === email,
(user) => {
user.email = "newemail@email.com";
},
);
// Returns { success: false, errors: Record<keyof Schema | 'general', string> || { success: true, data: UserModel }
if (!update.success) {
error(406, update.errors);
};
const user = update.data;findAndUpdate: finds multiple records, update them, and return the updated models
// No Query
const update = await Users.findAndUpdate((user) => {
user.attempts = 3; // locks all users
});
// or, Predicate Search, updating the matching records
const update = await Users.findAndUpdate(
(record, id) => record.attempts >= 3,
(user) => {
user.attempts = 0; // unlocks all locked users
},
);
// Returns { success: false, errors: Record<keyof Schema | 'general', string> } || { success: true, data: Array<UserModel> }
if (!update.success) {
error(406, update.errors);
};
const users = update.data;findOneAndDelete: finds one record, delete it, and return its model
// ID Query, using a bigint
const deletion = await Users.findOneAndDelete(0n);
// or, Predicate Search, updating the first match
const deletion = await Users.findOneAndDelete((record, id) => record.email === email);
// Returns UserModel | undefinedfindAndDelete: finds multiple records, delete them, and return the deleted models
// Must supply query to reduce chance of collection deletion
// Predicate Search, deleting the matching records
const deletion = await Users.findAndDelete((record, id) => record.attempts >= 3);
// Returns { success: false, errors: Record<keyof Schema | 'general', string> } || { success: true, data: Array<UserModel> }
if (!deletion.success) {
error(406, update.errors);
};
const users = deletion.data;findOneAndMap: finds a record, transform it, and return transformed data
// ID Query, using a bigint
const users = await Users.findOneAndMap(0n, (user) => {
if (user.locked) return `User with email ${user.email} locked`;
return `User with email ${user.email} unlocked`;
});
// or, Predicate Search, transforming the first matching model
const users = await Users.findOneAndMap(
(record, id) => record.attempts >= 3,
(user) => {
if (user.locked) return `User with email ${user.email} locked`;
return `User with email ${user.email} unlocked`;
},
);
// Returns UserModel | undefined if no matchesfindAndMap: finds multiple records, map over them, and return the mutated data
// No Parameter, mapping over all models from the database
const users = await Users.findAndMap((user) => {
if (user.locked) return `User with email ${user.email} locked`;
return `User with email ${user.email} unlocked`;
});
// or, Predicate Search, returning all matching models from the database
const users = await Users.findAndMap(
(record, id) => record.attempts >= 3,
(user) => {
if (user.locked) return `User with email ${user.email} locked`;
return `User with email ${user.email} unlocked`;
});
// Pagination
const users = await Users.findAndMap(
(user) => {
if (user.locked) return `User with email ${user.email} locked`;
return `User with email ${user.email} unlocked`;
},
{ offset: 5, limit: 10 },
);
// or
const users = await Users.findAndMap(
(user, id) => user.attempts >= 3,
(user) => {
if (user.locked) return `User with email ${user.email} locked`;
return `User with email ${user.email} unlocked`;
},
{ offset: 5, limit: 10 },
);
// Returns Array<UserModel> | undefined if no matches or pagination is out of boundscount: returns a count of how many records match
// No Query, returning the total number of records
const users = await Users.count();
// or, Predicate Search, returning the number of matches
const unlocked = await Users.count((user, id) => user.attempts < 3);exists: returns a count of how many records match
// ID Query, using bigint
const exists = await Users.exists(0n);
// or, Predicate Search, returning on the first match
const exists = await Users.exists((user, id) => user.email === 'email@email.com');flush: force a flush of in-memory records to the disk
await Users.flush();Though collections do not share relationships, the behavior of relationships can be mimiced
const message = await Users.findOneAndMap(
(user, id) => {
return user.email === "email@email.com";
},
async (user) => {
const profile = await Profiles.findOne(user.profileID);
if (profile) return `Welcome, ${profile.fullname}`;
},
);Found a bug or have an idea? Open an issue or PR.
MIT — © 2025 neisanworks