HotupdaterHot Updater
Database Adapters

AWS DynamoDB

Run Hot Updater's database on one DynamoDB table.

Installation

npm install @hot-updater/aws --save-dev

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

npm install @hot-updater/server @hot-updater/aws

Create the table

The managed AWS setup supports an AI agent or the interactive CLI. Both workflows create or validate the DynamoDB table, and write its schema settings, before configuring the app.

For custom installations, create one on-demand table keyed by string pk and sk, with no secondary index:

aws dynamodb create-table \
  --table-name hot-updater \
  --billing-mode PAY_PER_REQUEST \
  --deletion-protection-enabled \
  --on-demand-throughput MaxReadRequestUnits=4000,MaxWriteRequestUnits=100 \
  --attribute-definitions \
    AttributeName=pk,AttributeType=S \
    AttributeName=sk,AttributeType=S \
  --key-schema \
    AttributeName=pk,KeyType=HASH \
    AttributeName=sk,KeyType=RANGE

Turn on its TTL for _ttl. The adapter writes _ttl, in epoch seconds, on every item of a row with retention, such as Insights events and rollups, and DynamoDB deletes expired items without write cost:

aws dynamodb update-time-to-live \
  --table-name hot-updater \
  --time-to-live-specification Enabled=true,AttributeName=_ttl

Then write the schema settings with migrateDynamoDB. It takes the same configuration as dynamoDB, and an explicit array of the plugins the server runs. This example includes the managed server's API keys, Insights, and Remote Config.

import { migrateDynamoDB } from "@hot-updater/aws";
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";

await migrateDynamoDB(
  { tableName: "hot-updater", region: "us-east-1" },
  [apiKeys(), insights(), remoteConfig()],
);

When the table is missing, migrateDynamoDB also creates it, on demand and without deletion protection, which suits DynamoDB Local. Either way it turns on the table's TTL for _ttl when TTL is off. To write the settings with the AWS CLI instead, use dynamodb/schema-settings.json from hot-updater infra scaffold --provider aws; see Infrastructure Templates.

hot-updater db migrate, run with the file that exports a server's hotUpdater, does the same for the server's plugins. Their tables are key prefixes in the same table, so they need nothing else.

Schema settings

The settings are items under the partition private_hot_updater_settings: 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.

hot-updater init refuses an existing table of another shape, such as one with a secondary index.

How the table is used

dynamoDB(config) returns an engine database, { name, adapter, dispose }, which defineConfig and createHotUpdater both accept. Its adapter is the key-value helper over the table, behind the schema settings check:

  • Each row is one item whose pk is its table's name. Each index a row belongs to adds an item holding a copy of it, whose pk starts with <table>#, written in the same transaction.
  • Reads are strongly consistent. A unique lookup, such as an API key by its hash, is one point read.
  • Each write is one TransactWriteItems call with a client request token.

The table 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. dispose() closes the DynamoDB and CloudFront clients.

Batched Insights totals

DynamoDB bills every item a write touches, so dynamoDB 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 rows and one compressed log item, under the partitions aggregate_log_0 to aggregate_log_7.
  • A compaction merges the pending log items into the totals, one write for each total and one lock item, aggregate_lease, which lists the log items it applied. It then deletes them with BatchWriteItem. 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.

On DynamoDB Local, where nine events in ten are a returning installation's first launch of the day, an event costs about 8 write units at 10 or more events a second, and 32 to 37 with aggregateBatching: false.

OptionBehavior
aggregateBatching: {}The default: log items and a compaction every windowMs (60 seconds). Safe on Lambda, Lambda@Edge, and any other runtime.
aggregateBatching: { mode: "memory" }A long-lived server keeps totals in the process and writes them every windowMs (15 seconds), with no log items. A crash loses up to one window; call hotUpdater.flush() before the process exits.
aggregateBatching: falseWrites the totals in each event's transaction.

Configuration

import type { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";

interface DynamoDBConfig extends DynamoDBClientConfig {
  tableName: string;
  cloudfrontDistributionId?: string;
  apiBasePath?: string; // default: "/release-catalogs"
  shouldWaitForInvalidation?: boolean; // default: false
  aggregateBatching?:
    | { mode?: "log" | "memory"; windowMs?: number }
    | false; // default: {}, see Batched Insights totals
}

The other options, such as region, endpoint and credentials, configure the AWS SDK's DynamoDB client. If credentials is omitted, the AWS SDK default credential chain is used. This lets you rely on local AWS CLI sessions, AWS_PROFILE, shared config files, or other standard AWS credential sources.

When cloudfrontDistributionId is set, each committed write that changes a Release Catalog, such as a deploy or a Release policy change, invalidates <apiBasePath>/* on that distribution, /release-catalogs/* by default. Writes that change no Catalog, such as Insights events or API keys, invalidate nothing. Override apiBasePath only when a custom deployment mounts the catalog routes elsewhere. A failed invalidation logs a warning, and the write still succeeds. The invalidation also clears the cached 404 of a scope that had no catalog yet, which CloudFront otherwise keeps for its error caching minimum TTL, ten seconds by default. Set shouldWaitForInvalidation: true when a deployment or E2E flow must wait for CloudFront propagation; it waits up to five minutes.

Usage

hot-updater.config.ts (excerpt)
import { dynamoDB } from "@hot-updater/aws"; 
import { defineConfig } from "hot-updater";

export default defineConfig({
  // ... build, storage, plugins, and updateStrategy
  database: dynamoDB({ 
    tableName: "hot-updater", 
    region: "us-east-1", 
    credentials: { 
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 
    }, 
  }), 
});

Complete example

This is the hot-updater.config.ts that hot-updater init writes for managed AWS with the current AWS CLI session. plugins is the list the managed server runs:

hot-updater.config.ts
import { dynamoDB, s3Storage } from "@hot-updater/aws";
import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
import { bare } from "@hot-updater/bare";
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
import { defineConfig } from "hot-updater";
import { existsSync } from "node:fs";

if (existsSync(".env.hotupdater")) {
  process.loadEnvFile(".env.hotupdater");
}

const awsOptions = {
  region: process.env.HOT_UPDATER_S3_REGION!,
  credentials: fromNodeProviderChain(),
};

export default defineConfig({
  build: bare({ enableHermes: true }),
  storage: s3Storage({
    ...awsOptions,
    bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
  }),
  database: dynamoDB({
    ...awsOptions,
    tableName: process.env.HOT_UPDATER_DYNAMODB_TABLE_NAME!,
    cloudfrontDistributionId: process.env.HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID!,
  }),
  plugins: [apiKeys(), insights(), remoteConfig()],
  updateStrategy: "appVersion", // or "fingerprint"
});

Custom server usage

Use dynamoDB with createHotUpdater for self-hosted servers:

src/hotUpdater.ts (excerpt)
import { dynamoDB } from "@hot-updater/aws"; 
import { createHotUpdater } from "@hot-updater/server";
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";

export const hotUpdater = createHotUpdater({
  database: dynamoDB({ 
    tableName: process.env.AWS_DYNAMODB_TABLE_NAME!, 
    region: process.env.AWS_REGION!, 
    credentials: { 
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 
    }, 
  }), 
  storage, // The adapter hot-updater.config.ts uploads with, such as s3Storage(...).
  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 AWS server runs the same list. List the server's plugins in hot-updater.config.ts too; see CLI Configuration.

Permissions

The Lambda or server role reads with BatchGetItem and Query, writes with TransactWriteItems, which needs PutItem, UpdateItem, DeleteItem, and ConditionCheckItem on the table, and deletes applied Insights log items with BatchWriteItem. Without BatchWriteItem, the compaction deletes them through TransactWriteItems at twice the cost, and logs one warning. Every item's partition key is a table name, or a table name followed by # for its index items, so a dynamodb:LeadingKeys condition can scope the role to Hot Updater's items. The managed setup's policy lists them, including aggregate_log_0 to aggregate_log_7 and aggregate_lease. migrateDynamoDB also needs DescribeTable and DescribeTimeToLive, CreateTable when it creates the table, and UpdateTimeToLive when it enables TTL.

Transaction limits

DynamoDB permits at most 100 items and 4 MB in one transaction, 400 KB in one item, and keys of 2,048 bytes for pk and 1,024 bytes for sk. A transaction carries its rows' index items, reference counters and aggregate rows. A deploy writes each Bundle with its patches, its Release and the next Catalog of its scope. A write over the limits is refused before anything is written.

On this page