Understand
Queries and secondary indexes
ThimbleDB supports bounded document queries inside one scope and one collection. It does not provide SQL, joins, aggregates, cross-scope queries, or automatic indexes for every field.
Typed collection
Define the document parser and the indexes the authority should maintain:
import {
collectionIndexConfiguration,
defineCollection,
defineIndex,
} from "thimbledb";
type Note = {
id: string;
title: string;
body: string;
lastModified: number;
};
const noteSchema = {
parse(value: unknown): Note {
// Use Zod or another parser in a real application.
return value as Note;
},
};
export const notes = defineCollection("notes", noteSchema, {
indexes: [
defineIndex<Note>("by-title", ["title"]),
defineIndex<Note>(
"by-last-modified",
["lastModified"],
"range",
),
],
});
export const collectionIndexes =
collectionIndexConfiguration([notes]);
The parser uses the same structural parse(value) contract as Zod. ThimbleDB
does not require Zod as a runtime dependency.
Configure the authority
Cloudflare:
import {
createCloudflareAuthority,
} from "thimbledb/authority/cloudflare";
import { collectionIndexes } from "./collections";
export default createCloudflareAuthority({
collectionLayouts: {
notes: "snapshot",
},
collectionIndexes,
});
Node:
import {
startNodeAuthority,
} from "thimbledb/authority/node";
import { collectionIndexes } from "./collections";
await startNodeAuthority({
collectionLayouts: {
notes: "snapshot",
},
collectionIndexes,
});
Environment-only deployments can set the same configuration as JSON:
$env:THIMBLE_COLLECTION_INDEXES = '{"notes":[{"name":"by-title","fields":["title"],"mode":"equality"},{"name":"by-last-modified","fields":["lastModified"],"mode":"range"}]}'
Code configuration is easier to review and type-check.
Fluent query
const db = await createThimbleClient();
const noteCollection = db.collection(notes);
const result = await noteCollection
.where((note) => note.title.eq("abc"))
.orderBy((note) => note.lastModified.asc())
.take(25)
.get();
console.log(result.plan); // "index"
console.log(result.indexName); // "by-title"
console.log(result.documents);
The callback builds a serialisable query expression. ThimbleDB does not parse or execute arbitrary JavaScript on the authority.
Supported expressions:
eqneltltegtgteincontainsfor strings and arrays- boolean
and,or, andnotthrough the versioned query AST - up to four ordering fields
- a result limit from 1 to 1,000
ID equality is planned as a direct document read.
Local predicates
An arbitrary JavaScript predicate can only run after documents are loaded:
const urgent = await noteCollection.filter(
(note) => note.title.startsWith("Urgent"),
500,
);
filter is explicitly local and requires a maximum collection size. It cannot
use a secondary index.
How indexes are stored
Each configured index is an encrypted immutable index page. The page maps canonical scalar values to sorted document IDs.
The collection HEAD references:
- the active snapshot or trie root
- every active secondary index hash
- one collection revision
Documents and indexes become visible through the same conditional HEAD update. A stale writer cannot publish a document root without the matching index roots.
Failed conditional writes may leave unreachable immutable objects. Quiescent retention maintenance removes them later.
Equality, range, and composite indexes
Equality index:
defineIndex<Note>("by-title", ["title"]);
Range index:
defineIndex<Note>(
"by-last-modified",
["lastModified"],
"range",
);
Composite equality index:
defineIndex<Note>(
"by-owner-status",
["ownerId", "status"],
);
Range indexes contain exactly one field. Equality indexes can contain up to four fields.
Only scalar string, number, boolean, or null values are indexed. Arrays and objects remain available to bounded local filtering.
Query plans
Inspect the planned operation before running it:
const query = noteCollection
.where((note) => note.title.eq("abc"))
.take(25);
console.log(query.explain());
Plans are:
point: direct ID readindex: configured secondary index candidate lookupscan: bounded collection scan
Index candidates are still checked against the full query after their documents are read.
Existing collections
Adding an index changes the authority layout generation. Existing browsers must reload.
Removing an active index or changing its definition requires the explicit index migration. Ordinary writes and other maintenance operations fail closed when the configured definitions omit or disagree with an index referenced by the current collection HEAD.
Rebuild indexes during maintenance:
$env:THIMBLE_MIGRATION_QUIESCENT = "true"
$env:THIMBLE_SCOPE_ID = "user:<id>"
$env:THIMBLE_COLLECTIONS = "notes"
$env:THIMBLE_COLLECTION_LAYOUTS = "notes=snapshot"
$env:THIMBLE_COLLECTION_INDEXES = '{"notes":[{"name":"by-title","fields":["title"],"mode":"equality"}]}'
npx thimbledb rebuild-indexes
The migration exports and rewrites the same stored records, rebuilds indexes, and verifies document equality.
If an index is configured but has not been built yet, queries fall back to the bounded scan plan.
Deliberate limits
Secondary indexes do not turn ThimbleDB into Cosmos DB, PostgreSQL, or another general query engine.
ThimbleDB does not provide:
- automatic indexing of every field
- joins
- aggregates
- cross-collection or cross-scope queries
- distributed transactions
- arbitrary server-side JavaScript
- unrestricted regular expressions
- full-text or vector search
Choose a full database when those capabilities are central to the application.