Cloudflare D1 Database
Run Hot Updater's database on Cloudflare D1, a serverless SQLite database running at the edge.
Installation
npm install @hot-updater/cloudflare --save-devFor a Worker or other custom server, install the runtime dependencies in the server project:
npm install @hot-updater/server @hot-updater/cloudflareSetup
Set up the backend with an AI agent or the interactive CLI, then use the configuration below. Managed setup applies the schema for you.
d1Database connects to a database whose schema already exists; it does not
create tables when it serves. To prepare a database yourself, apply the
installed @hot-updater/cloudflare package's sql/bundles.sql to a new, empty
D1 database:
npx wrangler d1 execute <database-name> --remote \
--file node_modules/@hot-updater/cloudflare/sql/bundles.sqlThe same SQL ships as the Worker migration
worker/migrations/0001_hot-updater_1.0.0.sql, which managed setup applies
with wrangler d1 migrations apply. It creates the tables and indexes of core
and of the managed Worker's plugins, Insights, API keys, and Remote Config, and
the _hu_write
table that batch writes use, then writes the schema settings rows last.
A Worker that runs other
plugins
also needs their tables. hot-updater db generate writes a D1 migration of
core's tables and the plugins' tables to
migrations/<timestamp>_hot-updater.sql; apply it with
wrangler d1 migrations apply. hot-updater db migrate applies the same schema
through the Cloudflare API instead. Both read a Node module that passes the
Worker's plugins to createHotUpdater with the REST d1Database from
@hot-updater/cloudflare, and neither drops the tables of a plugin you remove.
Schema settings
Before its first read or write, the database checks the settings rows
schema.engine and schema.core in one batch read, and the Worker checks
schema.<id> for each plugin it runs, such as schema.insights and
schema.apiKeys, in another. While one is missing or different, it throws
HotUpdaterSchemaMigrationRequiredError, which names the setting, and the
Worker's handlers answer 503. A running Worker checks again on its next
request, so it recovers once the migration has run.
How D1 is used
d1Database returns an engine database, { name, adapter }, which
defineConfig and createHotUpdater both accept. Its adapter is Hot
Updater's shared SQL core for SQLite, behind the schema settings check.
D1 has no interactive transactions, so each write is one atomic batch. The
batch first records each operation's guard in _hu_write, against the rows as
they were before any change, then applies the changes only when no guard
failed, and finally reads and removes the result. A read that binds many
values is split to stay within D1's 100 parameters per statement, and one
write sends at most 450 operations, within a Worker invocation's 1,000 queries
on the paid plan.
The root export reaches D1 through the Cloudflare REST API, for
hot-updater.config.ts and the Console. It sends each value as JSON text and
reads it back with json_extract. Inside a Worker, use the binding version
below.
Configuration
interface D1DatabaseConfig {
databaseId: string; // D1 database ID
accountId: string; // Cloudflare account ID
cloudflareApiToken: string; // Cloudflare API token
}Usage
These CLI examples load the HOT_UPDATER_CLOUDFLARE_* variables written to
.env.hotupdater by managed setup. Keep that file's existing values.
import { existsSync } from "node:fs";
import { d1Database } from "@hot-updater/cloudflare";
import { defineConfig } from "hot-updater";
if (existsSync(".env.hotupdater")) {
process.loadEnvFile(".env.hotupdater");
}
export default defineConfig({
// ... build, storage, plugins, and updateStrategy
database: d1Database({
databaseId: process.env.HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID!,
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
cloudflareApiToken: process.env.HOT_UPDATER_CLOUDFLARE_API_TOKEN!,
}),
});Worker runtime usage
The root export above is for hot-updater.config.ts and the Console. Inside a
Cloudflare Worker, import the same provider name from the /worker subpath and
pass the native D1 binding at the createHotUpdater boundary:
import { d1Database } from "@hot-updater/cloudflare/worker";
import { createHotUpdater } from "@hot-updater/server";
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
import { env } from "cloudflare:workers";
export const hotUpdater = createHotUpdater({
database: d1Database(env.DB),
storage, // The adapter hot-updater.config.ts uploads with, such as r2Storage(...).
plugins: [insights(), apiKeys(), remoteConfig()],
});insights() records app events, apiKeys() protects client routes with
the x-api-key header, and remoteConfig() serves Remote Config. The managed
Worker runs the same list.
hot-updater.config.ts runs in Node, where cloudflare:workers doesn't exist.
Give it the root d1Database, which reaches the same database through the
REST API, and the Worker's plugins, or reach the Worker's admin handler with
standaloneRepository.
Rows past their tables' retention, such as Insights events and rollups, need no cron trigger. The Worker request whose write takes the hourly lease first deletes a bounded batch of them, within D1's query limit for one invocation.
Complete example
The hot-updater.config.ts that hot-updater init writes for managed
Cloudflare, with R2 storage, the D1 database, and the plugins the managed Worker
runs. r2Storage records the r2:// locations that the managed Worker
resolves:
import { bare } from "@hot-updater/bare";
import { d1Database, r2Storage } from "@hot-updater/cloudflare";
import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
import { defineConfig } from "hot-updater";
import { existsSync } from "node:fs";
if (existsSync(".env.hotupdater")) {
process.loadEnvFile(".env.hotupdater");
}
export default defineConfig({
build: bare({ enableHermes: true }),
storage: r2Storage({
bucketName: process.env.HOT_UPDATER_CLOUDFLARE_R2_BUCKET_NAME!,
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
credentials: {
accessKeyId: process.env.HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY!,
},
}),
database: d1Database({
databaseId: process.env.HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID!,
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
cloudflareApiToken: process.env.HOT_UPDATER_CLOUDFLARE_API_TOKEN!,
}),
plugins: [apiKeys(), insights(), remoteConfig()],
updateStrategy: "appVersion", // or "fingerprint"
});