Bun SQLite

@moduletrace/bun-sqlite

A schema-driven SQLite manager for Bun, featuring automatic schema synchronization, type-safe CRUD operations, vector embedding support (via sqlite-vec), and TypeScript type definition generation.


Table of Contents


Features

  • Schema-first design — define your database in TypeScript; the library syncs your SQLite file to match
  • Automatic migrations — adds new columns, recreates tables for complex changes, drops removed tables
  • Type-safe CRUD — fully generic select, insert, update, delete functions with TypeScript generics
  • Rich query DSL — filtering, ordering, pagination, joins, grouping, full-text search, sub-query counts
  • Vector table support — create and manage sqlite-vec virtual tables for AI/ML embeddings
  • TypeScript codegen — generate .ts type definitions from your schema automatically
  • Zero-config defaults — id, created_at, and updated_at fields are added to every table automatically

Prerequisites

This works for just bun, so bun (>= 1.3) should be installed.


Installation

There are two ways to install BunSQLite:

Via the official npm registry

@moduletrace/bun-sqlite is published to a private Gitea npm registry. You must configure your package manager to resolve the @moduletrace scope from that registry before installing.

Add the following to your project's .npmrc file (create it at the root of your project if it doesn't exist):

@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/

After this you can run

bun add @moduletrace/bun-sqlite

Via Github

To install directly from github simply run

bun add github:moduletrace/bun-sqlite

Quick Start

1. Create the config file

Create bun-sqlite.config.ts at your project root:

import type { BunSQLiteConfig } from "@moduletrace/bun-sqlite";

const config: BunSQLiteConfig = {
    db_name: "my-app.db",
    db_dir: "./db", // optional: where to store the db file
    db_backup_dir: ".backups", // optional: name of backups directory. Relative to the db dir.
    typedef_file_path: "./db/types/db.ts", // optional: where to write generated types
};

export default config;

2. Define your schema

Create ./db/schema.ts in db_dir (or schema.js, schema.json, schema.yaml, schema.yml):

import type { BUN_SQLITE_DatabaseSchemaType } from "@moduletrace/bun-sqlite";

const schema: BUN_SQLITE_DatabaseSchemaType = {
    dbName: "my-app",
    tables: [
        {
            tableName: "users",
            fields: [
                { fieldName: "first_name", dataType: "TEXT" },
                { fieldName: "last_name", dataType: "TEXT" },
                { fieldName: "email", dataType: "TEXT", unique: true },
            ],
        },
    ],
};

export default schema;

3. Sync the schema to SQLite

bunx bun-sqlite schema

This creates the SQLite database file and creates/updates all tables to match your schema.

4. Use the CRUD API

import BunSQLite from "@moduletrace/bun-sqlite";

// Insert
await BunSQLite.insert({
    table: "users",
    data: [{ first_name: "Alice", email: "[email protected]" }],
});

// Select
const result = await BunSQLite.select({ table: "users" });
console.log(result.payload); // Alice's row

// Update
await BunSQLite.update({
    table: "users",
    targetId: 1,
    data: { first_name: "Alicia" },
});

// Delete
await BunSQLite.delete({ table: "users", targetId: 1 });

Configuration

The config file is bun-sqlite.config.ts at the root of your project. bun-sqlite.config.js, .json, .yaml and .yml also work; keep only one. .ts/.js files must export default the config.

Field Type Required Description
db_name string Yes Filename for the SQLite database (e.g. "app.db")
db_schema_file_name string No Schema filename relative to db_dir. Defaults to schema.ts|js|json|yaml|yml in db_dir
db_backup_dir string No Directory for database backups, relative to db_dir
db_dir string No Root directory for the database file and schema. Defaults to project root
typedef_file_path string No Output path for generated TypeScript types, relative to project root
max_backups number No Maximum number of backup files to keep. Oldest are deleted automatically. Defaults to 10
max_exports number No Maximum number of archives kept in {db_dir}/.exports by export. Defaults to 10
wal_mode boolean No Enable SQLite WAL journal mode

Schema Definition

Database Schema

interface BUN_SQLITE_DatabaseSchemaType {
    dbName?: string;
    tables: BUN_SQLITE_TableSchemaType[];
}

Table Schema

interface BUN_SQLITE_TableSchemaType {
    tableName: string;
    tableDescription?: string;
    fields: BUN_SQLITE_FieldSchemaType[];
    indexes?: BUN_SQLITE_IndexSchemaType[];
    uniqueConstraints?: BUN_SQLITE_UniqueConstraintSchemaType[];
    parentTableName?: string; // inherit fields from another table in the schema
    tableNameOld?: string; // rename: set this to the old name to trigger ALTER TABLE RENAME
    isVector?: boolean; // mark this as a sqlite-vec virtual table
    vectorType?: string; // virtual table type, defaults to "vec0"
}

Field Schema

type BUN_SQLITE_FieldSchemaType = {
    fieldName?: string;
    dataType: "TEXT" | "INTEGER";
    primaryKey?: boolean;
    autoIncrement?: boolean;
    notNullValue?: boolean;
    unique?: boolean;
    defaultValue?: string | number;
    defaultValueLiteral?: string; // raw SQL literal, e.g. "CURRENT_TIMESTAMP"
    foreignKey?: BUN_SQLITE_ForeignKeyType;
    isVector?: boolean; // vector embedding column
    vectorSize?: number; // embedding dimensions (default: 1536)
    sideCar?: boolean; // sqlite-vec "+" prefix for side-car columns
    updatedField?: boolean; // flag that this field definition has changed
};

Foreign Key

interface BUN_SQLITE_ForeignKeyType {
    destinationTableName: string;
    destinationTableColumnName: string;
    cascadeDelete?: boolean;
    cascadeUpdate?: boolean;
}

Index

interface BUN_SQLITE_IndexSchemaType {
    indexName?: string;
    indexType?: "regular" | "full_text" | "vector";
    indexTableFields?: { value: string; dataType: string }[];
}

Unique Constraint

interface BUN_SQLITE_UniqueConstraintSchemaType {
    constraintName?: string;
    constraintTableFields?: { value: string }[];
}

CLI Commands

The package provides a bun-sqlite CLI binary.

schema — Sync database to schema

bunx bun-sqlite schema [options]
Option Description
-v, --vector Drop and recreate all vector (sqlite-vec) virtual tables
-t, --typedef Also generate TypeScript type definitions after syncing
--dry-run Print the planned schema changes without applying them
-y, --yes Apply destructive changes (dropped tables / columns) without confirmation

Examples:

# Sync schema only
bunx bun-sqlite schema

# Sync schema and regenerate types
bunx bun-sqlite schema --typedef

# Sync schema, recreate vector tables, and regenerate types
bunx bun-sqlite schema --vector --typedef

# Review planned changes first
bunx bun-sqlite schema --dry-run

Only tables whose definition changed are rebuilt. Changes that discard data (dropping a table, or a column removed from the schema) are listed and need confirmation before they run. Non-interactive runs (CI, scripts) refuse them unless --yes is passed. The plan is exact: --dry-run runs the whole sync in a transaction and rolls it back.

typedef — Generate TypeScript types only

bunx bun-sqlite typedef

Reads the schema and writes TypeScript type definitions to the path configured in typedef_file_path.


backup — Back up the database

bunx bun-sqlite backup

Copies the current database file into db_backup_dir with a timestamped filename. After copying, the oldest backups are automatically pruned so the number of stored backups never exceeds max_backups (default: 10).

Example:

bunx bun-sqlite backup
# Backing up database ...
# DB Backup Success!

restore — Restore the database from a backup

bunx bun-sqlite restore

Presents an interactive list of available backups sorted by date (newest first). Select a backup to overwrite the current database file with it.

Example:

bunx bun-sqlite restore
# Restoring up database ...
# ? Select a backup: (Use arrow keys)
# ❯ Backup #1: Mon Mar 02 2026 14:30:00
#   Backup #2: Sun Mar 01 2026 09:15:42
# DB Restore Success!

If no backups exist, the command exits with an error and a reminder to run backup first.


export — Portable database + schema archive

# Write {db_name}-{timestamp}.tar.gz into {db_dir}/.exports
bunx bun-sqlite export

# Explicit path / format
bunx bun-sqlite export -o ./my-app.tar.gz
bunx bun-sqlite export -f zip

The archive holds database.sqlite (a consistent VACUUM INTO snapshot) and your schema file as schema.<ext>. Archives in .exports are trimmed to max_exports. .zip needs the zip binary.


import — Database file, SQL script, or export archive

# Interactive picker (from {db_dir}/.exports)
bunx bun-sqlite import

# Full export (database + schema)
bunx bun-sqlite import ./my-app.tar.gz

# Archive: database only / schema only
bunx bun-sqlite import ./my-app.tar.gz --sql-only
bunx bun-sqlite import ./my-app.tar.gz --schema-only

# A SQLite database file, or an SQL script (run into a fresh database)
bunx bun-sqlite import ./other.db
bunx bun-sqlite import ./dump.sql

The current database is backed up to db_backup_dir before it is replaced. A .ts/.js schema from an archive runs as code on the next load, so import asks before writing it (pass --yes to skip the prompt).


CRUD API

Import the default export:

import BunSQLite from "@moduletrace/bun-sqlite";

All methods return an APIResponseObject<T>:

{
    success: boolean;
    payload?: T[];           // array of rows (select; also insert/update)
    singleRes?: T;           // first row (select; also insert/update)
    count?: number;          // total count (when count: true)
    postInsertReturn?: {
        affectedRows?: number;
        insertId?: number;
    };
    error?: string;
    msg?: string;
    debug?: any;
}

Select

BunSQLite.select<T>({ table, query?, count?, targetId? })
Parameter Type Description
table string Table name
query ServerQueryParam<T> Query/filter options (see Query API)
count boolean Return row count instead of rows
targetId string | number Shorthand to filter by id

Examples:

// Get all users
const res = await BunSQLite.select({ table: "users" });

// Get by ID
const res = await BunSQLite.select({ table: "users", targetId: 42 });

// Filter with LIKE
const res = await BunSQLite.select<UserType>({
    table: "users",
    query: {
        query: {
            first_name: { value: "Ali", equality: "LIKE" },
        },
    },
});

// Count rows
const res = await BunSQLite.select({ table: "users", count: true });
console.log(res.count);

// Pagination
const res = await BunSQLite.select({
    table: "users",
    query: { limit: 10, page: 2 },
});

Insert

BunSQLite.insert<T>({ table, data, update_on_duplicate?, returning? });
Parameter Type Description
table string Table name
data T[] Array of row objects to insert
update_on_duplicate boolean Upsert via ON CONFLICT ... DO UPDATE
returning boolean Return the inserted record(s). Defaults to true

created_at and updated_at are set automatically to Date.now().

On success the inserted record(s) come back in singleRes (first row) and payload (all rows, for multi-row inserts). postInsertReturn is still populated for backwards compatibility.

Example:

const res = await BunSQLite.insert({
    table: "users",
    data: [
        { first_name: "Alice", last_name: "Smith", email: "[email protected]" },
        { first_name: "Bob", last_name: "Jones", email: "[email protected]" },
    ],
});

console.log(res.singleRes); // first inserted record (with its id)
console.log(res.payload); // every inserted record
console.log(res.postInsertReturn?.insertId); // last inserted row ID

Pass returning: false to skip RETURNING (e.g. when inserting very large BLOB/vector values); the response then only contains postInsertReturn.


Update

BunSQLite.update<T>({ table, data, query?, targetId?, returning? })
Parameter Type Description
table string Table name
data Partial<T> Fields to update
query ServerQueryParam<T> WHERE clause conditions
targetId string | number Shorthand to filter by id
returning boolean Return the updated record(s). Defaults to true

A WHERE clause is required. If no condition matches, success is false.

updated_at is set automatically to Date.now().

On success the updated record(s) come back in singleRes (first row) and payload. Vector tables fall back to postInsertReturn automatically, because SQLite does not support UPDATE ... RETURNING on virtual tables.

Examples:

// Update by ID
const res = await BunSQLite.update({
    table: "users",
    targetId: 1,
    data: { first_name: "Alicia" },
});

console.log(res.singleRes); // the updated record

// Update with custom query
await BunSQLite.update({
    table: "users",
    data: { last_name: "Doe" },
    query: {
        query: {
            email: { value: "[email protected]", equality: "EQUAL" },
        },
    },
});

Delete

BunSQLite.delete<T>({ table, query?, targetId? })
Parameter Type Description
table string Table name
query ServerQueryParam<T> WHERE clause conditions
targetId string | number Shorthand to filter by id

A WHERE clause is required. If no condition is provided, success is false.

Examples:

// Delete by ID
await BunSQLite.delete({ table: "users", targetId: 1 });

// Delete with condition
await BunSQLite.delete({
    table: "users",
    query: {
        query: {
            first_name: { value: "Ben", equality: "LIKE" },
        },
    },
});

Raw SQL

BunSQLite.sql<T>({ sql, values? })
Parameter Type Description
sql string Raw SQL statement
values (string | number)[] Parameterized values

SELECT statements return rows; all other statements return postInsertReturn.

Examples:

// SELECT
const res = await BunSQLite.sql<UserType>({ sql: "SELECT * FROM users" });
console.log(res.payload);

// INSERT with params
await BunSQLite.sql({
    sql: "INSERT INTO users (first_name, email) VALUES (?, ?)",
    values: ["Charlie", "[email protected]"],
});

Query API Reference

The query parameter on select, update, and delete accepts a ServerQueryParam<T> object:

type ServerQueryParam<T> = {
    query?: { [key in keyof T]: ServerQueryObject };
    selectFields?: (keyof T | { fieldName: keyof T; alias?: string })[];
    omitFields?: (keyof T)[];
    limit?: number;
    page?: number;
    offset?: number;
    order?:
        | { field: keyof T; strategy: "ASC" | "DESC" }
        | { field: keyof T; strategy: "ASC" | "DESC" }[];
    searchOperator?: "AND" | "OR"; // how multiple query fields are joined (default: AND)
    join?: ServerQueryParamsJoin[];
    group?:
        | keyof T
        | { field: keyof T; table?: string }
        | (keyof T | { field: keyof T; table?: string })[];
    countSubQueries?: ServerQueryParamsCount[];
    fullTextSearch?: {
        fields: (keyof T)[];
        searchTerm: string;
        scoreAlias: string;
    };
};

Equality Operators

Set equality on any query field to control the comparison:

Equality SQL Equivalent
EQUAL (default) =
NOT EQUAL !=
LIKE LIKE '%value%'
LIKE_RAW LIKE 'value' (no auto-wrapping)
LIKE_LOWER LOWER(field) LIKE '%value%'
NOT LIKE NOT LIKE '%value%'
GREATER THAN >
GREATER THAN OR EQUAL >=
LESS THAN <
LESS THAN OR EQUAL <=
IN IN (val1, val2, ...) — pass array as value
NOT IN NOT IN (...)
BETWEEN BETWEEN val1 AND val2 — pass [val1, val2] as value
IS NULL IS NULL
IS NOT NULL IS NOT NULL
MATCH sqlite-vec vector nearest-neighbor search

Example:

// Find users with email NOT NULL, ordered by created_at DESC, limit 20
const res = await BunSQLite.select<UserType>({
    table: "users",
    query: {
        query: {
            email: { equality: "IS NOT NULL" },
        },
        order: { field: "created_at", strategy: "DESC" },
        limit: 20,
    },
});

JOIN

const res = await BunSQLite.select({
    table: "posts",
    query: {
        join: [
            {
                joinType: "LEFT JOIN",
                tableName: "users",
                match: { source: "user_id", target: "id" },
                selectFields: ["first_name", "email"],
            },
        ],
    },
});

Vector Table Support

@moduletrace/bun-sqlite integrates with sqlite-vec for storing and querying vector embeddings.

Define a vector table in the schema

{
    tableName: "documents",
    isVector: true,
    vectorType: "vec0",   // defaults to "vec0"
    fields: [
        {
            fieldName: "embedding",
            dataType: "TEXT",
            isVector: true,
            vectorSize: 1536,   // embedding dimensions
        },
        {
            fieldName: "title",
            dataType: "TEXT",
            sideCar: true,  // stored as a side-car column (+title) for efficiency
        },
        {
            fieldName: "body",
            dataType: "TEXT",
            sideCar: true,
        },
    ],
}

Side-car columns (sideCar: true) use sqlite-vec's +column syntax. They are stored separately from the vector index, keeping the index lean and fast while still being queryable alongside vector results.

Sync vector tables

# Initial sync
bunx bun-sqlite schema

# Recreate vector tables (e.g. after changing vectorSize)
bunx bun-sqlite schema --vector

Query vectors

const res = await BunSQLite.select({
    table: "documents",
    query: {
        query: {
            embedding: {
                equality: "MATCH",
                value: "<serialized-vector>",
                vector: true,
                vectorFunction: "vec_f32",
            },
        },
        limit: 5,
    },
});

TypeScript Type Generation

Run the typedef command (or pass --typedef to schema) to generate a .ts file containing:

  • A const array of all table names (BunSQLiteTables)
  • A type for each table (named BUN_SQLITE_<DB_NAME>_<TABLE_NAME>)
  • A union type BUN_SQLITE_<DB_NAME>_ALL_TYPEDEFS

Example output (db/types/db.ts):

export const BunSQLiteTables = ["users"] as const;

export type BUN_SQLITE_MY_APP_USERS = {
    /** The unique identifier of the record. */
    id?: number;
    /** The time when the record was created. (Unix Timestamp) */
    created_at?: number;
    /** The time when the record was updated. (Unix Timestamp) */
    updated_at?: number;
    first_name?: string;
    last_name?: string;
    email?: string;
};

export type BUN_SQLITE_MY_APP_ALL_TYPEDEFS = BUN_SQLITE_MY_APP_USERS;

Use the generated types with the CRUD API for full type safety:

import BunSQLite from "@moduletrace/bun-sqlite";
import { BUN_SQLITE_MY_APP_USERS, BunSQLiteTables } from "./db/types/db";

const res = await BunSQLite.select<BUN_SQLITE_MY_APP_USERS>({
    table: "users" as (typeof BunSQLiteTables)[number],
});

Default Fields

Every table automatically receives the following fields — you do not need to declare them in your schema:

Field Type Description
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL Unique row identifier
created_at INTEGER Unix timestamp set on insert
updated_at INTEGER Unix timestamp updated on every update

Project Structure

bun-sqlite/
├── src/
│   ├── index.ts                        # Main export (BunSQLite object)
│   ├── commands/
│   │   ├── index.ts                    # CLI entry point
│   │   ├── schema.ts                   # `schema` command
│   │   ├── typedef.ts                  # `typedef` command
│   │   ├── backup.ts                   # `backup` command
│   │   ├── restore.ts                  # `restore` command
│   │   ├── export.ts                   # `export` command
│   │   └── import.ts                   # `import` command
│   ├── functions/
│   │   ├── init.ts                     # Loads config, schema and client into globals
│   │   └── grab-sqlite-client.ts       # Database client (bun:sqlite + sqlite-vec)
│   ├── lib/
│   │   ├── db-handler.ts               # Runs every CRUD query
│   │   └── schema/                     # Schema synchronization (one step per file, + tests)
│   ├── lib/sqlite/
│   │   ├── db-select.ts                # Select implementation
│   │   ├── db-insert.ts                # Insert implementation
│   │   ├── db-update.ts                # Update implementation
│   │   ├── db-delete.ts                # Delete implementation
│   │   ├── db-sql.ts                   # Raw SQL implementation
│   │   ├── db-generate-type-defs.ts    # Type def generator
│   │   └── schema-to-typedef.ts        # Schema-to-TypeScript converter
│   ├── types/
│   │   └── index.ts                    # All TypeScript types and interfaces
│   └── utils/
│       ├── sql-generator.ts            # SELECT query builder
│       ├── sql-insert-generator.ts     # INSERT query builder
│       ├── sql-gen-operator-gen.ts     # Equality operator mapper
│       ├── sql-equality-parser.ts      # Equality string parser
│       ├── append-default-fields-to-db-schema.ts
│       ├── resolve-and-load-data-file.ts # Load .ts/.js/.json/.yaml config and schema files
│       ├── export-archive.ts           # Read/write export archives
│       ├── grab-db-dir.ts              # Resolve db/backup/export directory paths
│       ├── grab-db-backup-file-name.ts # Generate timestamped backup filename
│       ├── grab-sorted-backups.ts      # List backups sorted newest-first
│       ├── grab-backup-data.ts         # Parse metadata from a backup filename
│       └── trim-backups.ts             # Prune oldest backups over max_backups
└── test/
    └── test-01/                        # Example project using the library
        ├── bun-sqlite.config.ts
        ├── db/
        │   ├── bun-sqlite-schema.ts
        │   └── types/bun-sqlite.ts     # Generated types
        └── src/
            ├── insert.ts
            ├── select.ts
            ├── delete.ts
            └── sql.ts

Testing

bun run test

Runs the unit tests (tests/) and the schema sync tests (src/lib/schema/*.test.ts) against in-memory SQLite databases. No setup needed.


License

MIT

S
Description
No description provided
Readme
1.3 MiB
Languages
TypeScript 99.5%
Shell 0.5%