AWS DynamoDB
Run Hot Updater's database on one DynamoDB table.
Installation
npm install @hot-updater/aws --save-devFor a custom server, install the runtime dependency in the server project:
npm install @hot-updater/server @hot-updater/awsCreate 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=RANGETurn 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=_ttlThen 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
pkis its table's name. Each index a row belongs to adds an item holding a copy of it, whosepkstarts 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
TransactWriteItemscall 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_0toaggregate_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 withBatchWriteItem. 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.
| Option | Behavior |
|---|---|
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: false | Writes 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
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:
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:
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.