HotupdaterHot Updater
Database Adapters

Firestore Database

Run Hot Updater's database in one Cloud Firestore collection.

Installation

npm install @hot-updater/firebase firebase-admin --save-dev

For a custom server, install the runtime dependencies in the server project:

npm install @hot-updater/server @hot-updater/firebase firebase-admin

Setup

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 pk and sk. 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 runTransaction that 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 pk and sk at 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.

.env.hotupdater
# 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-name

Usage

hot-updater.config.ts (excerpt)
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:

hot-updater.config.ts
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:

src/hotUpdater.ts (excerpt)
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

On this page