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.
Choose one deployment shape:
- run the authority in-app inside the application deployment for the smallest operational surface
- run the authority as a separate Worker, container, function, or Node service for independent secret, release, failure, and scaling boundaries
In either case, preserve one public browser origin through the application server or a path-based gateway. See In-app and separate authority deployment.
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.
Open http://127.0.0.1:5173/studio/ to inspect the same local authority
through the packaged management frontend.
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
Browser clients, Cloudflare Worker deployments, and local-file Node authorities do not need either cloud 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_READ_BUNDLES": "true",
"THIMBLE_COLLECTION_LAYOUTS": "",
"THIMBLE_COLLECTION_INDEXES": "{}",
"THIMBLE_DELETE_RETENTION_DAYS": "30",
"THIMBLE_DELETE_GRACE_DAYS": "7",
"THIMBLE_MAINTENANCE_MODE": "false",
"THIMBLE_ALLOWED_ORIGIN": "https://database.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": "database.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({
readBundles: true,
});
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.
For the complete option and environment-variable matrix, see
Configuration reference.
Both authority examples explicitly enable read bundles. This lets the trusted authority return bounded decoded cache values over HTTPS. Disable the option if every application-data response above TLS must remain a TDB1 envelope. See Security.
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"],
"equality",
{
include: ["body", "lastModified"],
},
),
defineIndex<Note>(
"by-last-modified",
["lastModified"],
"range",
),
],
});
const noteCardSchema = {
parse(value: unknown): Pick<
Note,
"id" | "title" | "body" | "lastModified"
> {
return value as Pick<
Note,
"id" | "title" | "body" | "lastModified"
>;
},
};
const result = await db
.collection(notes)
.where((note) => note.title.eq("First note"))
.orderBy((note) => note.lastModified.desc())
.take(25)
.select(
["title", "body", "lastModified"],
noteCardSchema,
)
.get();
The authority must configure the same indexes. See Queries and secondary indexes.
On an eligible cold point read, the ready client uses the bounded bundle endpoint advertised by a version 3.1 authority. Older or oversized deployments automatically use the individual object path.
Advanced manual client construction
Applications that need custom readers or caches can construct every component directly:
import {
EnvelopeJsonObjectReader,
HttpByteObjectReader,
HttpPointReadBundleReader,
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,
...(config.readBundleBaseUrl
? {
bundleReader: new HttpPointReadBundleReader(
config.readBundleBaseUrl,
config.scope.id,
),
}
: {}),
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. - Eligible cold point reads use one bounded bundle request and oversized bundles fall back to the object path.
- 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.