Start here
Quickstart
ThimbleDB requires an external OIDC provider. It does not store passwords, passkeys, MFA factors, or recovery credentials.
Choose one authority:
- Cloudflare Worker with R2 for the reference deployment
- Node authority with local, Azure Blob, S3, or R2 storage
The browser API is the same for every authority.
Fast local evaluation
Generate a complete local Node authority and Vite application:
npx thimbledb@latest create my-notes-app
cd my-notes-app
npm run dev
Open http://127.0.0.1:5173. The scaffold uses the loopback-only development
identity, encrypted local scopes, typed notes, declared indexes, deletion, and
restore.
See Local development before replacing the development identity with production OIDC.
Standalone repository templates:
Manual installation
npm install thimbledb
ThimbleDB is ESM-only and requires Node.js 22 or newer for Node deployments. The base package does not install Azure or AWS SDKs.
For a Node authority backed by Azure Blob:
npm install @azure/storage-blob
For a Node authority backed by Amazon S3 or R2 through the S3 API:
npm install @aws-sdk/client-s3
Cloudflare Worker, browser-only, and local Node deployments do not need either provider SDK.
Cloudflare Worker and R2
Create src/worker.ts:
export { default } from "thimbledb/authority/cloudflare";
Create two private R2 buckets:
npx wrangler r2 bucket create <data-bucket>
npx wrangler r2 bucket create <auth-bucket>
Generate the Entra delegated scope and application roles once:
npx thimbledb generate-entra-roles `
--out ".\entra-authorization.json"
Merge the generated entries with the existing application registration. Keep the generated IDs stable after assignments are created.
Use a Wrangler configuration with caller-owned resources:
{
"name": "my-thimbledb-authority",
"main": "src/worker.ts",
"compatibility_date": "2026-09-23",
"workers_dev": false,
"vars": {
"THIMBLE_PREFIX": "prod",
"THIMBLE_KEY_VERSION": "1",
"THIMBLE_READ_KEY_VERSIONS": "",
"THIMBLE_HEAD_TTL_MS": "10000",
"THIMBLE_COLLECTION_LAYOUTS": "",
"THIMBLE_COLLECTION_INDEXES": "{}",
"THIMBLE_RETIRED_COLLECTION_LAYOUTS": "",
"THIMBLE_DELETE_RETENTION_DAYS": "30",
"THIMBLE_DELETE_GRACE_DAYS": "7",
"THIMBLE_MAINTENANCE_MODE": "false",
"THIMBLE_ALLOWED_ORIGIN": "https://app.example.com",
"ENTRA_TENANT_ID": "<tenant-id>",
"ENTRA_AUDIENCE": "<api-client-id>",
"ENTRA_REQUIRED_SCOPE": "thimble.access",
"ENTRA_REQUIRED_ROLE": "thimble.user"
},
"r2_buckets": [
{
"binding": "DB",
"bucket_name": "<data-bucket>"
},
{
"binding": "AUTH_DB",
"bucket_name": "<auth-bucket>"
}
],
"routes": [
{
"pattern": "db.example.com",
"custom_domain": true
}
]
}
Configuring both ENTRA_REQUIRED_SCOPE and ENTRA_REQUIRED_ROLE requires
both claims. To accept Entra client-credentials tokens, use a required
application role without a delegated-scope requirement. See
Machine and service access.
Generate and store the master key without writing it to source:
$masterKey = node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
$masterKey | npx wrangler secret put THIMBLE_MASTER_KEY
$masterKey = $null
Deploy:
npx wrangler deploy
Apply the auth-session lifecycle rules from
deploy/cloudflare/auth-lifecycle.example.json. Do not apply an independent
age-based lifecycle rule to the data bucket because it cannot determine which
immutable objects remain reachable.
Node authority
Create server.mjs:
import { startNodeAuthority } from "thimbledb/authority/node";
await startNodeAuthority();
For local development:
$env:THIMBLE_PROVIDER = "local"
$env:THIMBLE_ALLOWED_ORIGIN = "http://127.0.0.1:5173"
$env:OIDC_PROVIDER_ID = "my-provider"
$env:OIDC_ISSUER = "https://identity.example.com/"
$env:OIDC_AUDIENCE = "thimbledb-api"
$env:OIDC_JWKS_URI = "https://identity.example.com/.well-known/jwks.json"
$env:OIDC_REQUIRED_SCOPE = "thimble.access"
node server.mjs
The local adapter stores application objects under .thimble-data and auth
records under .thimble-auth. It is intended for one Node process.
Before selecting azure, install @azure/storage-blob. Before selecting s3
or r2, install @aws-sdk/client-s3. For provider environment variables,
trusted proxy, and secure-cookie configuration, use the deployment guides.
Browser client
Obtain an API access token through the application’s OIDC authorization-code flow with PKCE. Do not hard-code or persist the access token.
Exchange it for a ThimbleDB session:
await fetch("/api/auth/oidc/entra/session", {
method: "POST",
credentials: "same-origin",
headers: {
"content-type": "application/json",
authorization: `Bearer ${accessToken}`,
},
body: "{}",
});
Create the ready client:
import { createThimbleClient } from "thimbledb";
const db = await createThimbleClient();
Define and query a typed collection:
import {
defineCollection,
defineIndex,
} from "thimbledb";
type Note = {
id: string;
title: string;
body: string;
lastModified: number;
};
const notes = defineCollection<Note>("notes", {
indexes: [
defineIndex<Note>("by-title", ["title"]),
defineIndex<Note>(
"by-last-modified",
["lastModified"],
"range",
),
],
});
const result = await db
.collection(notes)
.where((note) => note.title.eq("First note"))
.orderBy((note) => note.lastModified.desc())
.take(25)
.get();
The authority must configure the same indexes. See Queries and secondary indexes.
Advanced manual client construction
Applications that need custom readers or caches can construct every component directly:
import {
EnvelopeJsonObjectReader,
HttpByteObjectReader,
IndexedDbObjectCache,
MemoryObjectCache,
ScopedJsonObjectReader,
ThimbleClient,
TieredObjectCache,
base64ToBytes,
importAesGcmKey,
} from "thimbledb";
const config = await fetch("/api/config", {
credentials: "same-origin",
cache: "no-store",
}).then((response) => response.json());
const grant = await fetch(config.scope.keyEndpoint, {
credentials: "same-origin",
cache: "no-store",
}).then((response) => response.json());
const keys = new Map();
for (const item of grant.keys) {
const raw = base64ToBytes(item.key);
keys.set(
item.keyId,
await importAesGcmKey(raw, ["decrypt"], false),
);
raw.fill(0);
}
const reader = new ScopedJsonObjectReader(
new EnvelopeJsonObjectReader(
new HttpByteObjectReader(config.readBaseUrl),
(keyId) => keys.get(keyId) ?? null,
),
config.scope.id,
);
const cache = new TieredObjectCache(
new MemoryObjectCache(),
new IndexedDbObjectCache(
`${location.origin}:${config.scope.id}`,
),
config.cachePolicy,
);
const db = new ThimbleClient({
reader,
cache,
headTtlMs: config.headTtlMs,
csrfToken: config.csrfToken,
scopeId: config.scope.id,
keyExpiresAt: grant.expiresAt,
collectionLayouts: config.collectionLayouts,
layoutGeneration: config.layoutGeneration,
configurationUrl: "/api/config",
onLayoutChange: () => location.reload(),
});
Read and mutate documents:
await db.write("notes", "note-1", {
id: "note-1",
title: "First note",
body: "Stored through ThimbleDB",
});
const note = await db.get("notes", "note-1");
const notes = await db.scan("notes");
await db.delete("notes", "note-1");
await db.restore("notes", "note-1");
Verify the integration
Confirm:
/api/auth/configlists the expected OIDC provider.- The session cookie is HttpOnly, SameSite=Strict, and Secure in production.
/api/configreturns the expected internal user scope.- Brokered objects begin with
TDB1. - R2, S3, or Blob credentials never reach the browser.
- Scope keys exist only as non-extractable in-memory CryptoKeys.
- Logout blocks later object reads and clears the browser cache.
- Delete and restore follow the configured retention window.
- A stale layout generation receives
409 layout_changed.
Continue with Authentication, Local development, Queries and secondary indexes, Logical migration, Deletion and retention, and Adaptive layouts.