HotupdaterHot Updater
Database Plugins

Standalone Database

Connect your CLI to a self-hosted Hot Updater server.

Overview

The standaloneRepository plugin connects your Hot Updater CLI to a self-hosted backend server. This is the client-side configuration for connecting to your server.

Building a Self-Hosted Server?

See the Self-Hosting (Custom) Guide for complete server implementation instructions using @hot-updater/server with database adapters (Drizzle, Prisma, Kysely, MongoDB) and frameworks (Hono, Express, Elysia).

Installation

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

Usage

Configure your CLI to connect to your self-hosted server:

hot-updater.config.ts
import { standaloneRepository } from "@hot-updater/standalone";
import { defineConfig } from "hot-updater";

export default defineConfig({
  build: /* your build plugin */,
  storage: /* your storage plugin */,
  database: standaloneRepository({
    baseUrl: "http://localhost:3000/hot-updater",
  }),
  updateStrategy: "appVersion",
});

Configuration

The standaloneRepository plugin accepts the following options:

interface StandaloneRepositoryConfig {
  baseUrl: string;                        // Your server URL
  commonHeaders?: Record<string, string>; // Optional headers (e.g., authentication)
  routes?: Routes;                        // Optional custom route configuration
}

routes lets you map the client to a different server shape:

interface RouteConfig {
  path: string;
  headers?: Record<string, string>;
}

interface Routes {
  create?: () => RouteConfig;
  update?: (bundleId: string) => RouteConfig;
  list?: () => RouteConfig;
  channels?: () => RouteConfig;
  retrieve?: (bundleId: string) => RouteConfig;
  delete?: (bundleId: string) => RouteConfig;
}

Basic Example

database: standaloneRepository({
  baseUrl: "http://localhost:3000/hot-updater",
})

With Authentication

database: standaloneRepository({
  baseUrl: process.env.HOT_UPDATER_SERVER_URL!,
  commonHeaders: {
    "Authorization": `Bearer ${process.env.API_TOKEN}`
  }
})

With Custom Routes

database: standaloneRepository({
  baseUrl: "https://api.example.com",
  routes: {
    create: () => ({
      path: "/v1/hot-updater/api/bundles",
      headers: { "X-Custom-Header": "value" }
    }),
    update: (bundleId) => ({
      path: `/v1/hot-updater/api/bundles/${bundleId}`,
      headers: { "X-Custom-Header": "value" }
    }),
    list: () => ({
      path: "/v1/hot-updater/api/bundles"
    }),
    channels: () => ({
      path: "/v1/hot-updater/api/bundles/channels"
    }),
    retrieve: (bundleId) => ({
      path: `/v1/hot-updater/api/bundles/${bundleId}`
    }),
    delete: (bundleId) => ({
      path: `/v1/hot-updater/api/bundles/${bundleId}`
    })
  }
})

Server Contract

standaloneRepository is an HTTP client. If you are building a custom backend, your server must implement the bundle-management contract below.

If you use @hot-updater/server, you do not need to implement these endpoints manually. createHotUpdater() provides this contract when bundle routes are explicitly enabled and authorized, including GET /api/bundles/channels.

Contract Types

interface PaginationInfo {
  total: number;
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  currentPage: number;
  totalPages: number;
  nextCursor?: string | null;
  previousCursor?: string | null;
}

interface DataResponse<TData> {
  data: TData;
}

interface Paginated<TData> extends DataResponse<TData> {
  pagination: PaginationInfo;
}

type PaginatedResult = Paginated<Bundle[]>;
type ChannelsResponse = DataResponse<{ channels: string[] }>;

Default Routes

These are the default routes used by standaloneRepository. You can remap them with the routes option shown above.

MethodDefault routeRequest bodySuccess response
POST/api/bundlesBundle[]{ "success": true }
PATCH/api/bundles/:idPartial<Bundle>{ "success": true }
GET/api/bundlesnonePaginated<Bundle[]>
GET/api/bundles/:idnoneBundle
DELETE/api/bundles/:idnone{ "success": true }
GET/api/bundles/channelsnoneChannelsResponse

Bundle Shape

Create requests send full Bundle objects. Update requests send Partial<Bundle> and, if an id is present in the body, it must match the :id route parameter.

interface Bundle {
  id: string;
  platform: "ios" | "android";
  shouldForceUpdate: boolean;
  enabled: boolean;
  fileHash: string;
  storageUri: string;
  gitCommitHash: string | null;
  message: string | null;
  channel: string;
  targetAppVersion: string | null;
  fingerprintHash: string | null;
  metadata?: {
    app_version?: string;
  };
  rolloutCohortCount?: number | null;
  targetCohorts?: string[] | null;
}

List Bundles Contract

GET /api/bundles must accept the pagination and filter query params used by the console and standaloneRepository, and return a paginated JSON body.

Cursor-based pagination is the official contract. page is an optional positive integer used alongside cursors when the caller needs stable page numbers.

Supported query params:

Query paramTypeNotes
channelstringExact match
platform"ios" | "android"Exact match
enabledbooleanExact match
idEq, idGt, idGte, idLt, idLtestringBundle ID filters
idInstring[]Repeated parameter
targetAppVersionstring | nullExact match, or literal null
targetAppVersionInstring[]Repeated parameter
targetAppVersionNotNullbooleanExact match
fingerprintHashstring | nullExact match, or literal null
limitnumberWindow size
pagenumberOptional positive page number for stable page-aligned requests
afterstringFetch the next window after this bundle ID
beforestringFetch the previous window before this bundle ID

Array filters use repeated query parameters. Boolean values must be exactly true or false, and nullable string filters use the literal null.

Expected response:

{
  "data": [{ "id": "bundle-id", "channel": "production" }],
  "pagination": {
    "total": 1,
    "hasNextPage": false,
    "hasPreviousPage": false,
    "currentPage": 1,
    "totalPages": 1,
    "nextCursor": null,
    "previousCursor": null
  }
}

standaloneRepository reads pagination metadata from the response body. It does not derive pagination from headers. currentPage and totalPages are kept for backwards compatibility; nextCursor and previousCursor are the preferred navigation fields.

If your standalone server is backed by object storage or manifest files, keep GET /api/bundles on an index-backed path once the index is warm. Cursor pages should not require rescanning all manifests, and this should be covered by deterministic read-counter regression tests.

Retrieve And Channels Contract

  • GET /api/bundles/:id should return 200 with a Bundle body or 404 if the bundle does not exist.
  • GET /api/bundles/channels must return:
{
  "data": {
    "channels": ["production", "staging"]
  }
}

Minimal @hot-updater/server Example

If you want this contract without implementing it yourself, use @hot-updater/server:

import { createHotUpdater } from "@hot-updater/server";

const hotUpdater = createHotUpdater({
  database,
  storages: [storage],
  basePath: "/hot-updater",
  routes: {
    updateCheck: true,
    bundles: true,
  },
});

This mounts the bundle routes consumed by standaloneRepository under /hot-updater/api/bundles*. routes.bundles: true is the explicit opt-in for those management APIs; protect them at the framework or reverse proxy layer, and configure matching commonHeaders in standaloneRepository.

Next Steps

On this page