Firestore Database
Run Hot Updater's database in one Cloud Firestore collection.
Installation
npm install @hot-updater/firebase firebase-admin --save-devFor a custom server, install the runtime dependencies in the server project:
npm install @hot-updater/server @hot-updater/firebase firebase-adminSetup
Set up the backend with an AI agent or the interactive CLI, then use the configuration below.
Configuration
import type { AppOptions } from "firebase-admin/app";
type FirebaseDatabaseConfig = AppOptions & {
databaseId?: string; // A named Firestore database; defaults to "(default)"
collection?: string; // The one collection it uses; defaults to "hot_updater_v1"
aggregateBatching?:
| { mode?: "log" | "memory"; windowMs?: number }
| false; // default: {}, see Batched Insights totals
};Pass projectId and credential, as for any Firebase Admin app. The database
initializes the default Firebase Admin app with these options, or uses the
default app when the process has already initialized one.
Set databaseId to keep the collection in a named Firestore
database instead
of the project's (default) one. The database must already exist. Pass the
same databaseId to migrateFirebaseDatabase and wherever the server and
hot-updater.config.ts create firebaseDatabase. Managed init asks for the
database and records it as HOT_UPDATER_FIREBASE_DATABASE_ID.
Collection and indexes
The database keeps every row in one collection, hot_updater_v1 by default, as
{ pk, sk, row } documents. Each index a row belongs to adds a document holding
a copy of it, written in the same transaction. Reads use two composite indexes,
on pk ascending with sk ascending and descending, and row is exempt from
single-field indexing. firestore.indexes.json from the managed setup declares
both for hot_updater_v1; deploy it and wait until the indexes are ready
before serving traffic. If you set collection, declare the same indexes and
exemption for that collection instead. If you set databaseId, deploy them to
that database: give the firestore entry of firebase.json its database
field.
Every document of a row with retention,
such as Insights events and rollups, carries an expireAt timestamp beside
pk and sk. The same file declares a TTL policy on expireAt, exempt from
indexing, and Firestore deletes those documents, usually within a day of
their expiry. Declare it too if you set collection or databaseId.
Then write the schema settings the database checks before its first read:
import { migrateFirebaseDatabase } from '@hot-updater/firebase';
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
import { applicationDefault } from 'firebase-admin/app';
await migrateFirebaseDatabase(
{
projectId: 'your-project-id',
credential: applicationDefault(),
},
[apiKeys(), insights(), remoteConfig()],
);migrateFirebaseDatabase takes the same configuration as firebaseDatabase,
and an explicit array of the plugins the server runs: here the managed
server's API keys, Insights, and Remote Config. hot-updater init
and the managed
setup's key script both run it. hot-updater db migrate, run with the file
that exports a server's hotUpdater, writes the same settings for the
server's plugins,
whose tables need no collection or index of their own.
Schema settings
The settings are the rows schema.engine, schema.core, and one
schema.<id> for each plugin the server runs, such as schema.insights and
schema.apiKeys. Before its first read or write, the database checks core's
rows in one batch read, and the server checks its plugins' rows in another.
While one is missing or different, it throws
HotUpdaterSchemaMigrationRequiredError, which names the setting, and the
server's handlers answer 503. A running server checks again on its next
request, so it recovers without a restart once the settings exist.
How the collection is used
firebaseDatabase(config) returns an engine database,
{ name, adapter, dispose }, which defineConfig and createHotUpdater both
accept. Its adapter is the key-value helper over the collection, behind the
schema settings check:
- Each document's id is a hash of its
pkandsk. Maps and arrays are stored as JSON text, since Firestore has no nested arrays and does not keep map key order. - A write is one
runTransactionthat reads only the documents its operations guard. Counters increment without a read. - A transaction holds at most 500 writes and 10 MB, a document at most 1 MB,
and
pkandskat most 1,500 bytes each. A write over these limits is refused before anything is written.
The collection holds core's rows (Bundles, patches, Releases, Release Catalogs, Channels and their counters) and the rows of the plugins the server runs, such as Insights, API keys, and Remote Config.
Batched Insights totals
Firestore bills each document write, so firebaseDatabase batches Insights'
totals (its counters, gauges, and sketches) instead of writing them in each
event's transaction:
- The event's transaction writes the event's own documents and one compressed log document.
- A compaction merges pending log documents into the totals, one write for each total and one lock document, which lists the log documents it applied. It then deletes them in a plain batch. So no crash loses an event's totals or counts one twice.
- After an event, the server compacts when a minute has passed since the last compaction. A read of the totals, such as the Console's, compacts first. If it cannot write, it shows the stored totals, which can leave out pending events until a later compaction succeeds.
Set aggregateBatching: { mode: "memory" } on a long-lived server to keep
totals in the process and write them every 15 seconds, without log documents.
A crash then loses up to one window; call hotUpdater.flush() before the
process exits. aggregateBatching: false writes the totals in each event's
transaction.
Environment variables
Use an existing application-default identity when available. Set
GOOGLE_APPLICATION_CREDENTIALS only when you need a private credential file;
keep the file outside the app bundle and exclude local environment files from
Git. A Firebase CLI login alone does not necessarily authenticate the local
Admin SDK.
# Optional: a private credential file when existing ADC is unavailable
# GOOGLE_APPLICATION_CREDENTIALS=/private/path/to/your-service-account-key.json
FIREBASE_PROJECT_ID=your-project-id
# Required for the combined storage example below
FIREBASE_STORAGE_BUCKET=your-bucket-nameUsage
import { existsSync } from "node:fs";
import { firebaseDatabase } from "@hot-updater/firebase";
import { applicationDefault } from "firebase-admin/app";
import { defineConfig } from "hot-updater";
if (existsSync(".env.hotupdater")) {
process.loadEnvFile(".env.hotupdater");
}
// Reuse the environment's application-default credentials.
const credential = applicationDefault();
export default defineConfig({
// ... build, storage, plugins, and updateStrategy
database: firebaseDatabase({
projectId: process.env.FIREBASE_PROJECT_ID!,
credential,
}),
});Complete example
Combined storage and database, with the plugins the managed Cloud Function runs:
import { existsSync } from "node:fs";
import { bare } from "@hot-updater/bare";
import { firebaseDatabase, firebaseStorage } from "@hot-updater/firebase";
import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
import { applicationDefault } from "firebase-admin/app";
import { defineConfig } from "hot-updater";
if (existsSync(".env.hotupdater")) {
process.loadEnvFile(".env.hotupdater");
}
// Reuse the environment's application-default credentials.
const credential = applicationDefault();
export default defineConfig({
build: bare({ enableHermes: true }),
storage: firebaseStorage({
projectId: process.env.FIREBASE_PROJECT_ID!,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET!,
credential,
}),
database: firebaseDatabase({
projectId: process.env.FIREBASE_PROJECT_ID!,
credential,
}),
plugins: [apiKeys(), insights(), remoteConfig()],
updateStrategy: "appVersion",
});Custom server usage
Use firebaseDatabase with createHotUpdater for self-hosted servers:
import { firebaseDatabase } from "@hot-updater/firebase";
import { createHotUpdater } from "@hot-updater/server";
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
import { applicationDefault } from "firebase-admin/app";
export const hotUpdater = createHotUpdater({
database: firebaseDatabase({
projectId: process.env.FIREBASE_PROJECT_ID!,
credential: applicationDefault(),
}),
storage, // The adapter hot-updater.config.ts uploads with, such as firebaseStorage(...).
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
Cloud Function runs the same list. List the server's plugins in
hot-updater.config.ts too; see
CLI Configuration.
Dependencies
Requires firebase-admin 11.10.0 or later as a peer dependency, the first
version that opens named databases:
npm install firebase-admin --save-dev