Node Sqlite Vec logo

Node Sqlite Vec

Community
mizchi
node-sqlite-vec

Use when setting up Node 24+ built-in `node:sqlite` with the loadable `sqlite-vec` extension for vector / RAG storage in TypeScript without `better-sqlite3`, or when debugging vec0 BigInt rowids, vitest hanging on sqlite-vec, `.ts` import errors under `node:sqlite`, or "sqlite extension not loadable". Covers extension loading, vitest incompatibility (use `node --test`), tsconfig flags, and CLI shebang. Trigger on `node:sqlite`, `sqlite-vec`, vec0 / vector storage even when the user does not name better-sqlite3.

Overview

Publishermizchi
Repositoryskills
Skill namenode-sqlite-vec
Stars
333
Forks
4
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by mizchi on GitHub. Read the source before you install it.

Installation

Install the Node Sqlite Vec AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/mizchi/skills.git /tmp/skills
mkdir -p .claude/skills
cp -r /tmp/skills/node-sqlite-vec .claude/skills/node-sqlite-vec
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Node Sqlite Vec in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Node Sqlite Vec on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Node Sqlite Vec is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

node-sqlite-vec

Recipe for combining Node 24's built-in node:sqlite with the sqlite-vec extension. Each section below is a known pitfall paired with the working incantation — read the Pitfalls section first if you only have a minute.

When to use

  • Adding vector search / RAG storage to a Node 24+ project
  • You want to avoid the native build dance of better-sqlite3
  • TypeScript source that runs directly with --experimental-strip-types

Skip this skill if:

  • Your runtime is Node ≤ 22 (no built-in node:sqlite) — use better-sqlite3
  • You need server-grade concurrency / replication — SQLite is the wrong tool

Pitfalls (read first)

SymptomCauseFix
Failed to load url sqlite (resolved id: sqlite) from vitestvite strips the node: prefix and looks up plain sqlite, which does not existDrop vitest. Use node --test (built-in, no vite layer)
ERR_SQLITE_ERROR: Only integers are allows for primary key values on <table>_vec on insertJS number gets bound as REAL; vec0 virtual tables require strict integer rowidsWrap rowid bindings with BigInt(id)
loadExtension is not a function or "Not authorized to use load_extension"Both extension-load gates must be openednew DatabaseSync(path, { allowExtension: true }) and db.enableLoadExtension(true)
ExperimentalWarning: SQLite is an experimental feature printed every CLI runNode 24 emits this for any node:sqlite import#!/usr/bin/env -S node --no-warnings=ExperimentalWarning in the bin shebang
An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabledtsc rejects import "./x.ts" by default; --experimental-strip-types requires .ts extensionsSet allowImportingTsExtensions: true and rewriteRelativeImportExtensions: true in tsconfig.json

Setup

Dependencies

sh
pnpm add sqlite-vec
pnpm add -D @types/node typescript

No better-sqlite3, no bindings, no node-gyp.

tsconfig.json

jsonc
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "allowImportingTsExtensions": true,    // import "./x.ts" allowed in source
    "rewriteRelativeImportExtensions": true, // tsc emits "./x.js" in output
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  }
}

rewriteRelativeImportExtensions lets you keep .ts imports in source (so node --experimental-strip-types tests/x.test.ts works) while still producing valid .js output via tsc.

package.json scripts

jsonc
{
  "scripts": {
    "build": "tsc",
    "test": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
    "typecheck": "tsc --noEmit"
  }
}

Do not pass --experimental-sqlite. The flag stopped being required in Node v22.13.0 / v23.4.0 — node:sqlite is importable out of the box on every version this skill targets, and the flag is now a no-op. Node still prints an ExperimentalWarning because the module sits at stability 1.2 (release candidate as of Node 25.7); see "Silencing the ExperimentalWarning" below.

--experimental-strip-types is a separate flag and is still needed to run .ts files directly on Node 22; Node 23+ strips types by default.

Open + load extension

ts
// src/db.ts
import { DatabaseSync } from "node:sqlite";
import * as sqliteVec from "sqlite-vec";

export function openDB(path: string) {
  const db = new DatabaseSync(path, { allowExtension: true });
  db.enableLoadExtension(true);
  sqliteVec.load(db);
  db.enableLoadExtension(false); // re-disable after load (defense in depth)
  return db;
}

Both allowExtension: true (constructor) and enableLoadExtension(true) (instance) are required. If either is missing, the load throws.

Verify:

ts
const row = db.prepare("SELECT vec_version() AS v").get();
console.log(row.v); // → "v0.1.9"

Schema: vec0 virtual table

ts
const EMBEDDING_DIM = 1024;
db.exec(`
  CREATE TABLE IF NOT EXISTS items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    body TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
  );
`);
db.exec(
  `CREATE VIRTUAL TABLE IF NOT EXISTS item_vec USING vec0(embedding FLOAT[${EMBEDDING_DIM}]);`,
);

Keep the regular table (items) and the vector table (item_vec) joined by idrowid.

Insert: the BigInt rowid trap

ts
const insertItem = db.prepare(
  `INSERT INTO items (body) VALUES (?) RETURNING id`,
);
const insertVec = db.prepare(
  `INSERT INTO item_vec (rowid, embedding) VALUES (?, ?)`,
);

function add(body: string, embedding: Float32Array): number {
  const { id } = insertItem.get(body) as { id: number };

  // ❌ insertVec.run(id, blob)
  //    → ERR_SQLITE_ERROR: Only integers are allows for primary key values on item_vec
  //
  // ✅ Wrap with BigInt to force INTEGER affinity:
  insertVec.run(
    BigInt(id),
    new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength),
  );
  return id;
}

Apply the same BigInt(id) wrap to every vec0 binding: DELETE FROM item_vec WHERE rowid = ?, etc. The regular items table does not need this — only vec0 virtual tables enforce strict integer rowids.

The embedding goes in as a raw little-endian float buffer (Uint8Array view over a Float32Array).

KNN query

ts
const sql = `
  SELECT i.id, i.body, v.distance
  FROM item_vec AS v
  JOIN items AS i ON i.id = v.rowid
  WHERE v.embedding MATCH ? AND v.k = ?
  ORDER BY v.distance ASC
  LIMIT ?
`;
const blob = new Uint8Array(query.buffer, query.byteOffset, query.byteLength);
const rows = db.prepare(sql).all(blob, k, k);

v.k = ? pre-filters to the top-k inside vec0; the outer LIMIT is the same number for clarity. When you also need a non-vector filter (e.g. WHERE kind = 'X'), pull more candidates via a larger v.k so the secondary filter does not starve.

Testing: do NOT use vitest

vitest depends on vite, and vite's resolver strips the node: prefix when resolving imports. node:sqlite has no plain-name fallback, so vitest fails before any test runs:

Error: Failed to load url sqlite (resolved id: sqlite). Does the file exist?

Use Node's built-in test runner instead:

ts
// tests/db.test.ts
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { openDB } from "../src/db.ts";

let dir: string;
let db: ReturnType<typeof openDB>;

beforeEach(() => {
  dir = mkdtempSync(join(tmpdir(), "spike-"));
  db = openDB(join(dir, "test.db"));
});
afterEach(() => {
  db.close();
  rmSync(dir, { recursive: true, force: true });
});

describe("vec0", () => {
  it("loads sqlite-vec", () => {
    const row = db.prepare("SELECT vec_version() AS v").get() as { v: string };
    assert.match(row.v, /^v\d+\.\d+\.\d+/);
  });
});
sh
node --experimental-strip-types --test tests/*.test.ts

Real DB, no mocks. node:test outputs TAP by default; pass --test-reporter=spec for the readable form.

CLI shebang

ts
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
import { DatabaseSync } from "node:sqlite";
// ...

env -S splits the rest of the shebang on whitespace so you can pass flags. Without --no-warnings=ExperimentalWarning, every CLI invocation prints:

(node:12345) ExperimentalWarning: SQLite is an experimental feature and might change at any time

This becomes annoying in scripts that capture stderr. Suppress it at the entry point only — keep it visible during development if you want.

Stability timeline

Nodenode:sqlite status
v22.5.0Added, behind --experimental-sqlite
v22.13.0 / v23.4.0Flag no longer required; still experimental (warning stays)
v25.7.0+Stability 1.2 — release candidate; warning still emitted

Practical consequences today (Node 24 LTS "Krypton", Node 26 current):

  • Drop --experimental-sqlite from every script — it does nothing
  • Keep --no-warnings=ExperimentalWarning while the module is pre-1.0; remove it once the stability index reaches 2
  • Everything else (BigInt rowids, vitest avoidance, tsconfig flags) is independent of stability and continues to apply

References

Frequently asked questions

What does the Node Sqlite Vec AI skill do?

Use when setting up Node 24+ built-in `node:sqlite` with the loadable `sqlite-vec` extension for vector / RAG storage in TypeScript without `better-sqlite3`, or when debugging vec0 BigInt rowids, vitest hanging on sqlite-vec, `.ts` import errors under `node:sqlite`, or "sqlite extension not loadable". Covers extension loading, vitest incompatibility (use `node --test`), tsconfig flags, and CLI shebang. Trigger on `node:sqlite`, `sqlite-vec`, vec0 / vector storage even when the user does not name better-sqlite3.

Why use Node Sqlite Vec on TypingMind?

Because you install it once and use it with any model. Node Sqlite Vec is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Node Sqlite Vec in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/mizchi/skills/tree/main/node-sqlite-vec. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Node Sqlite Vec?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Node Sqlite Vec?

As many as you like. As long as a model supports skills, you can use Node Sqlite Vec with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Node Sqlite Vec AI skill free?

It is published on GitHub by mizchi. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇