SuperAPI AI Directory

vibe-coding-db-bettersqlite

Coding & development

View repository

Updated 10-15 16:44

About this skill

Vibe Coding — BetterSQLite3

Synchronous SQLite for Node.js. Faster and simpler than the async sqlite3 package.

Entry Router

Electron/Tauri project?
→ READ: ## Setup (includes native module rebuild steps)

Using Drizzle ORM?
→ READ: ## Drizzle Integration

Raw SQL patterns needed?
→ READ: ## CRUD Patterns + ## Transactions

Performance/WAL setup?
→ READ: ## Performance Pragmas

Why BetterSQLite3 over sqlite3

Feature better-sqlite3 sqlite3
API style Synchronous Callback/Promise
Performance 2-3x faster Baseline
Transactions Native, synchronous Manual, async
Prepared statements First-class, reusable Verbose
Electron support With rebuild step With rebuild step

Setup

npm install better-sqlite3
npm install --save-dev @types/better-sqlite3

Electron / Tauri — rebuild for Electron's Node version:

npm install --save-dev electron-rebuild
npx electron-rebuild -f -w better-sqlite3

# Or with electron-builder (add to package.json):
"build": {
  "extraMetadata": {},
  "npmRebuild": true
}

Connection and Pragmas

import Database from "better-sqlite3";

// Create or open database
const db = new Database("app.db");

// Essential pragmas — run once on connection
db.pragma("journal_mode = WAL");       // concurrent reads during writes
db.pragma("synchronous = NORMAL");     // safe + fast
db.pragma("foreign_keys = ON");        // enforce FK constraints
db.pragma("cache_size = -32000");      // 32MB cache
db.pragma("temp_store = MEMORY");      // temp ops in RAM
db.pragma("mmap_size = 268435456");    // 256MB memory-mapped I/O

// For Electron — use app data dir, not project root
import { app } from "electron";
import path from "path";
const dbPath = path.join(app.getPath("userData"), "app.db");
const db = new Database(dbPath);

Schema and Migrations

// Run schema setup on startup
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id    INTEGER PRIMARY KEY,
    email TEXT    NOT NULL UNIQUE,
    name  TEXT,
    created_at INTEGER NOT NULL DEFAULT (unixepoch())
  );

  CREATE TABLE IF NOT EXISTS posts (
    id         INTEGER PRIMARY KEY,
    title      TEXT    NOT NULL,
    content    TEXT,
    published  INTEGER NOT NULL DEFAULT 0,
    author_id  INTEGER NOT NULL REFERENCES users(id),
    created_at INTEGER NOT NULL DEFAULT (unixepoch())
  );

  CREATE INDEX IF NOT EXISTS idx_posts_author ON posts(author_id);
  CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published, created_at);
`);

Versioned migrations:

function migrate(db: Database.Database) {
  db.exec(`CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)`);

  const row = db.prepare("SELECT version FROM _schema_version").get() as { version: number } | undefined;
  let version = row?.version ?? 0;

  const migrations: (() => void)[] = [
    // v1
    () => db.exec(`ALTER TABLE users ADD COLUMN avatar_url TEXT`),
    // v2
    () => db.exec(`CREATE INDEX idx_users_email ON users(email)`),
  ];

  db.transaction(() => {
    for (let i = version; i < migrations.length; i++) {
      migrations[i]();
      version = i + 1;
    }
    if (row) {
      db.prepare("UPDATE _schema_version SET version = ?").run(version);
    } else {
      db.prepare("INSERT INTO _schema_version (version) VALUES (?)").run(version);
    }
  })();
}

CRUD Patterns

// Prepare statements once, reuse many times
const insertUser = db.prepare(
  "INSERT INTO users (email, name) VALUES (@email, @name) RETURNING *"
);
const getUserByEmail = db.prepare(
  "SELECT * FROM users WHERE email = ?"
);
const listPosts = db.prepare(
  "SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC LIMIT ? OFFSET ?"
);
const updatePost = db.prepare(
  "UPDATE posts SET title = @title, content = @content WHERE id = @id"
);
const deletePost = db.prepare("DELETE FROM posts WHERE id = ?");

// Usage
const user = insertUser.get({ email: "user@example.com", name: "Alice" });
const found = getUserByEmail.get("user@example.com");
const posts = listPosts.all(20, 0);       // .all() returns array
updatePost.run({ title: "New Title", content: "...", id: 1 });
deletePost.run(1);

Transactions

Transactions are synchronous and fast — use them for any multi-step operation:

// Simple transaction
const createUserWithProfile = db.transaction((email: string, name: string) => {
  const user = db.prepare(
    "INSERT INTO users (email, name) VALUES (?, ?) RETURNING *"
  ).get(email, name) as User;

  db.prepare(
    "INSERT INTO profiles (user_id, bio) VALUES (?, '')"
  ).run(user.id);

  return user;
});

const newUser = createUserWithProfile("user@example.com", "Alice");

// Nested transactions (uses savepoints)
const outer = db.transaction(() => {
  // ...
  inner();  // inner transaction uses SAVEPOINT
});
const inner = db.transaction(() => { /* ... */ });

Drizzle ORM with BetterSQLite3

// src/db/index.ts
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import * as schema from "./schema";

const sqlite = new Database("app.db");
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("foreign_keys = ON");

export const db = drizzle(sqlite, { schema });

// Query with Drizzle
import { eq, desc, and } from "drizzle-orm";

// Select
const users = await db.select().from(schema.users);

// Insert
const [user] = await db.insert(schema.users)
  .values({ email: "user@example.com", name: "Alice" })
  .returning();

// Update
await db.update(schema.posts)
  .set({ published: true })
  .where(eq(schema.posts.id, 1));

// Delete
await db.delete(schema.posts).where(eq(schema.posts.id, 1));

// Join
const postsWithAuthors = await db
  .select({ post: schema.posts, author: schema.users })
  .from(schema.posts)
  .leftJoin(schema.users, eq(schema.posts.authorId, schema.users.id))
  .where(and(eq(schema.posts.published, true)))
  .orderBy(desc(schema.posts.createdAt))
  .limit(20);

Performance Tips

Batch inserts — use transactions:

const insertMany = db.transaction((rows: { email: string; name: string }[]) => {
  const stmt = db.prepare("INSERT INTO users (email, name) VALUES (?, ?)");
  for (const row of rows) stmt.run(row.email, row.name);
});
insertMany(largeArray);  // 100x faster than individual inserts

Read-heavy queries — use .pluck() for single column, .raw() for arrays:

const emails = db.prepare("SELECT email FROM users").pluck().all();
// Returns: ["a@x.com", "b@x.com"] instead of [{ email: "a@x.com" }]

const rows = db.prepare("SELECT id, email FROM users").raw().all();
// Returns: [[1, "a@x.com"], [2, "b@x.com"]] — faster, less memory

WAL checkpoint (for long-running apps):

// Periodically checkpoint WAL to main DB file
setInterval(() => {
  db.pragma("wal_checkpoint(TRUNCATE)");
}, 60_000);

TypeScript Types

import Database from "better-sqlite3";

interface User {
  id: number;
  email: string;
  name: string | null;
  created_at: number;
}

interface Post {
  id: number;
  title: string;
  content: string | null;
  published: number;   // 0 or 1 — SQLite has no boolean
  author_id: number;
  created_at: number;
}

// Type your prepared statements
const getUser = db.prepare<[number], User>("SELECT * FROM users WHERE id = ?");
const user = getUser.get(1);  // typed as User | undefined

Common Pitfalls

Native module in monorepo / pnpm If you get "Cannot find module 'better-sqlite3'" in Electron, run electron-rebuild — the native .node file must be compiled for Electron's exact Node version.

Boolean columns SQLite stores booleans as integers. published = true in JS needs to be 1 in SQLite. Drizzle handles this automatically with { mode: "boolean" }. Raw SQL: use 0/1 literals.

Dates Store as INTEGER (Unix epoch via unixepoch()) or TEXT (ISO 8601). Avoid REAL. Drizzle { mode: "timestamp" } handles the conversion automatically.

WAL and multiple processes WAL mode is per-connection. If multiple Node.js processes open the same .db file, each must set WAL separately. Writers still serialize — only one write at a time.


Progress.txt Update

After BetterSQLite3 setup is complete, append these fields to the active BUILD phase block in progress.txt:

PHASE: BUILD
STATUS: in_progress
database: bettersqlite
orm: [drizzle|none]
schema_complete: true
migrations_run: true
timestamp: [ISO timestamp]

vibe-coding-build reads schema_complete and migrations_run to confirm the database step is done before proceeding to the next IMPLEMENTATION_PLAN step.