HotupdaterHot Updater
Storage Adapters

Neon Object Storage

Store your Hot Updater bundles in a Neon Object Storage bucket, next to the Postgres database that tracks them.

Installation

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

For a deployed server, install the package as a runtime dependency instead of a development dependency.

Setup

Create a bucket on a branch of your Neon project. A private bucket works, because devices download from presigned URLs:

neon buckets create hot-updater-bundles --access-level private

Then write the branch's storage and Postgres variables to .env.hotupdater, and add that file to .gitignore:

neon env pull --file .env.hotupdater -s object-storage,postgres

The command writes AWS_ENDPOINT_URL_S3, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION for the bucket, and DATABASE_URL for the branch's database.

Configuration

import type { S3ClientConfig } from "@aws-sdk/client-s3";

interface NeonStorageConfig
  extends Omit<S3ClientConfig, "credentials" | "endpoint" | "forcePathStyle"> {
  endpoint: string; // The branch's storage endpoint (AWS_ENDPOINT_URL_S3)
  bucketName: string; // The bucket on that branch
  credentials: { accessKeyId: string; secretAccessKey: string };
  basePath?: string; // Optional base path within the bucket
  signedUrlExpiresIn?: number; // Download URLs, at most 604800 (7 days). Default: 3600 seconds
}

endpoint is the branch's host only, such as https://br-xxx.storage.c-5.us-east-2.aws.neon.tech. Pass the bucket as bucketName, not in the endpoint's path. The adapter always uses path-style requests, the only style Neon serves. region defaults to us-east-1, and Neon accepts a signature for any region.

Usage

@hot-updater/neon also exports postgres, so one package covers both the bundles and their metadata:

hot-updater.config.ts
import { existsSync } from "node:fs";

import { bare } from "@hot-updater/bare";
import { neonStorage, postgres } from "@hot-updater/neon"; 
import { defineConfig } from "hot-updater";
import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";

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

export default defineConfig({
  build: bare({ enableHermes: true }),
  updateStrategy: "appVersion",
  storage: neonStorage({ 
    endpoint: process.env.AWS_ENDPOINT_URL_S3!, 
    bucketName: "hot-updater-bundles", 
    credentials: { 
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 
    }, 
    region: process.env.AWS_REGION, 
  }), 
  database: postgres({ 
    connectionString: process.env.DATABASE_URL!, 
  }), 
  plugins: [apiKeys(), insights(), remoteConfig()],
});

Neon has no managed Hot Updater server, so the app checks for updates against a custom server that lists the same adapters, as in Custom server usage. Create the tables through that server file once:

npx hot-updater db migrate src/hotUpdater.ts

Protocol

neon://bucket/encoded/key

For example, neon://hot-updater-bundles/releases/1.0.0/bundle%20ios.zip is stored in the storageUri field. Keys retain their slash hierarchy and encode each segment. The URI names the bucket and not the branch, so a child branch resolves the URIs it inherits against its own copy of the bucket.

Capabilities

neonStorage implements upload, read, download URL, existence-check, exact-delete, listing, and batch-delete operations. Uploads above 5 MB go up in parts. A download URL is a presigned URL on the branch's endpoint that expires after signedUrlExpiresIn, so the bucket can stay private.

Branches

A Neon branch copies its parent's buckets along with its database. Point a preview or staging config at a child branch's AWS_ENDPOINT_URL_S3 and DATABASE_URL to deploy bundles there without touching the parent's bundles or release catalog.

Maintenance

neonStorage provides the object listing and exact deletion capabilities used by hot-updater storage prune. Follow the Storage Cleanup guide to remove unreferenced bundle objects and shared assets safely.

Use a separate basePath for every database that shares a bucket. Pruning assumes that the configured database owns every object below its storage prefix.

Custom server usage

Use neonStorage and postgres with createHotUpdater for self-hosted servers:

src/hotUpdater.ts
import { neonStorage, postgres } from "@hot-updater/neon"; 
import { createHotUpdater } from "@hot-updater/server";
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";

export const hotUpdater = createHotUpdater({
  database: postgres({ connectionString: process.env.DATABASE_URL! }), 
  plugins: [apiKeys(), insights(), remoteConfig()],
  storage: neonStorage({ 
    endpoint: process.env.AWS_ENDPOINT_URL_S3!, 
    bucketName: "hot-updater-bundles", 
    credentials: { 
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 
    }, 
    region: process.env.AWS_REGION, 
  }), 
  // ... other options
});

On serverless platforms, use the pooled DATABASE_URL.

On this page