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 { defineConfig } from "@hot-updater/core";
import { standaloneRepository } from "@hot-updater/standalone";

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

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() already provides this contract when bundle routes are enabled, 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
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

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",
});

This mounts the bundle routes consumed by standaloneRepository under /hot-updater/api/bundles* by default.

Next Steps