Database
VeloxKit embeds SQLite 3 with WAL mode enabled by default. The db API is fully async and works from any component or JS plugin.
Requires capability: add "db": true to veloxkit.config.json.
{
"capabilities": { "db": true }
}Opening a database
import { db } from '@velox/react';
// Call once on app start — auto-sets the default handle.
await db.open('app.db');
// Or keep the handle for multi-DB apps:
const usersDb = await db.open('users.db');
const logsDb = await db.open('logs.db');
db.setDefault(logsDb);db.open() creates the file if it doesn't exist. Use ":memory:" for an in-memory database.
Migrations
db.migrate() tracks applied versions in _velox_migrations and only runs pending entries. Each migration is committed atomically together with its tracking record — a partial failure leaves the database clean.
await db.open('app.db');
await db.migrate([
{
version: 1,
name: 'create_notes',
up: `CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
pinned INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
)`,
},
{
version: 2,
name: 'add_fts',
// Multiple statements — pass an array:
up: [
`CREATE VIRTUAL TABLE notes_fts USING fts5(title, body, content=notes, content_rowid=id)`,
`CREATE TRIGGER notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
END`,
`CREATE TRIGGER notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, title, body) VALUES('delete', old.id, old.title, old.body);
INSERT INTO notes_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
END`,
`CREATE TRIGGER notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, title, body) VALUES('delete', old.id, old.title, old.body);
END`,
],
},
{
version: 3,
name: 'add_tags',
up: [
`CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)`,
`CREATE TABLE note_tags (
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (note_id, tag_id)
)`,
],
},
]);up is a string (single statement) or an array of strings (multiple statements). Migrations run in ascending version order; already-applied versions are skipped.
Call db.migrate() before rendering your first screen. The recommended pattern is to run it in a startup useEffect or in the root component before mounting children.
Inspecting migration history
The tracking table is just a regular SQLite table:
const history = await db.query('SELECT version, name, applied_at FROM _velox_migrations ORDER BY version');
console.log(history);
// [{ version: 1, name: 'create_notes', applied_at: 1718000000 }, ...]Seeding
db.seed() runs setup data after migrations. Two modes:
| Call | Behaviour |
|---|---|
db.seed(fn) | Always runs — write idempotent SQL (INSERT OR IGNORE) |
db.seed(name, fn) | Tracked in _velox_seeds, runs once per name |
// Always run — idempotent defaults:
await db.seed(async () => {
await db.run(
'INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)',
['theme', 'dark']
);
await db.run(
'INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)',
['font_size', '16']
);
});
// Run once — dev sample data:
await db.seed('sample_notes', async () => {
await db.run(
'INSERT INTO notes (title, body) VALUES (?, ?)',
['Welcome to VeloxKit', 'Edit this note or create a new one.']
);
});The tracked seed is skipped on every subsequent app launch.
Recommended startup pattern
Put migrations and seeding together in a single async function called before rendering:
// src/db.js
import { db } from '@velox/react';
export async function initDb() {
await db.open('app.db');
await db.migrate([
{ version: 1, name: 'initial_schema', up: `
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
)
`},
]);
await db.seed('sample_notes', async () => {
await db.run("INSERT INTO notes (title) VALUES (?)", ['My first note']);
});
}// src/App.jsx
import { useState, useEffect } from 'react';
import { View, Text } from '@velox/react';
import { initDb } from './db';
import { NotesScreen } from './NotesScreen';
export default function App() {
const [ready, setReady] = useState(false);
useEffect(() => {
initDb().then(() => setReady(true));
}, []);
if (!ready) return <View><Text>Loading…</Text></View>;
return <NotesScreen />;
}Query patterns
Select all rows
const notes = await db.query(
'SELECT * FROM notes ORDER BY updated_at DESC'
);Select with parameters
const note = await db.query(
'SELECT * FROM notes WHERE id = ?', [id]
);
// note[0] or undefinedInsert and get new ID
const { lastInsertId } = await db.run(
'INSERT INTO notes (title, body) VALUES (?, ?)',
['My note', 'Content here']
);Update
await db.run(
'UPDATE notes SET body = ?, updated_at = unixepoch() WHERE id = ?',
[newBody, id]
);Delete
await db.run('DELETE FROM notes WHERE id = ?', [id]);Full-text search (after FTS5 virtual table is set up)
async function searchNotes(query) {
if (!query.trim()) {
return db.query('SELECT * FROM notes ORDER BY updated_at DESC');
}
return db.query(
`SELECT notes.*
FROM notes_fts
JOIN notes ON notes.id = notes_fts.rowid
WHERE notes_fts MATCH ?
ORDER BY rank`,
[query]
);
}Atomic transaction
await db.transaction([
{ sql: 'INSERT INTO archived_notes SELECT * FROM notes WHERE id = ?', params: [id] },
{ sql: 'DELETE FROM notes WHERE id = ?', params: [id] },
]);Any statement failure rolls back the entire transaction.
Using a JS plugin (recommended for complex data layers)
For non-trivial apps, keep all SQL inside a JS plugin so the React side stays clean:
{
"capabilities": { "db": true },
"plugins": [
{ "entry": "src/plugins/notes.plugin.js", "name": "notes" }
]
}import { db } from '@velox/react';
await db.open('app.db');
await db.migrate([{ version: 1, name: 'create_notes', up: `CREATE TABLE notes (...)` }]);
await db.seed('defaults', async () => { /* ... */ });
export async function getAll() {
return db.query('SELECT * FROM notes ORDER BY updated_at DESC');
}
export async function create(args) {
const { lastInsertId } = await db.run(
'INSERT INTO notes (title) VALUES (?)', [args.title]
);
return { id: lastInsertId };
}import { backend } from '@velox/react';
const notes = await backend.notes.getAll();
await backend.notes.create({ title: 'New note' });Multiple databases
const mainDb = await db.open('main.db');
const cacheDb = await db.open('cache.db');
// Pass the handle explicitly for non-default databases:
await db.migrate(cacheDb, [
{ version: 1, up: 'CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT, ttl INTEGER)' },
]);
await db.run(cacheDb, 'INSERT INTO cache (key, value, ttl) VALUES (?, ?, ?)',
['user_profile', JSON.stringify(profile), Date.now() + 86400_000]);
// Close when done:
await db.close(cacheDb);