HotupdaterHot Updater
Storage Plugins

Custom Storage Plugin

Create your own storage plugin for any storage provider

Installation

npm install @hot-updater/plugin-core --save-dev

Overview

Build a custom storage plugin to integrate any storage provider. This guide shows how to use createStoragePlugin to build and configure your own storage plugin.

Creating a Storage Plugin

Use createStoragePlugin to build custom storage plugins:

import { createStoragePlugin } from "@hot-updater/plugin-core";

export const myStorage = createStoragePlugin<MyConfig>({
  name: "myStorage",              // Plugin identifier
  supportedProtocol: "custom",    // Storage URI protocol (e.g., "custom://...")
  factory: (config) => ({
    // Return these three required methods:
    upload: async (key, filePath) => ({ storageUri: "..." }),
    delete: async (storageUri) => { /* ... */ },
    getDownloadUrl: async (storageUri) => ({ fileUrl: "..." })
  })
});

Factory Function Return Type

Your factory function must return an object with these methods:

{
  // Uploads file to storage and returns storage URI
  upload: (key: string, filePath: string) => Promise<{ storageUri: string }>;
  // Deletes all files at the storage URI path
  delete: (storageUri: string) => Promise<void>;
  // Generates download URL for clients to fetch bundles
  getDownloadUrl: (storageUri: string) => Promise<{ fileUrl: string }>;
}

Implementation Example

Here's a complete custom storage plugin implementation:

customStorage.ts
import {
  DeleteObjectCommand,
  GetObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import {
  createStoragePlugin,
  getContentType,
  parseStorageUri,
} from "@hot-updater/plugin-core";
import fs from "fs/promises";
import path from "path";

export interface CustomStorageConfig {
  region: string;
  credentials: {
    accessKeyId: string;
    secretAccessKey: string;
  };
  bucketName: string;
}

export const customStorage = createStoragePlugin<CustomStorageConfig>({
  name: "customStorage",
  supportedProtocol: "s3",
  factory: (config) => {
    const { bucketName, ...s3Config } = config;
    const client = new S3Client(s3Config);

    return {
      async upload(key, filePath) {
        const Body = await fs.readFile(filePath);
        const ContentType = getContentType(filePath);
        const filename = path.basename(filePath);
        const Key = `${key}/${filename}`;

        const upload = new Upload({
          client,
          params: {
            Bucket: bucketName,
            Key,
            Body,
            ContentType,
          },
        });

        await upload.done();

        return {
          storageUri: `s3://${bucketName}/${Key}`,
        };
      },

      async delete(storageUri) {
        const { bucket, key } = parseStorageUri(storageUri, "s3");

        if (bucket !== bucketName) {
          throw new Error(`Bucket mismatch: expected "${bucketName}"`);
        }

        const command = new DeleteObjectCommand({
          Bucket: bucketName,
          Key: key,
        });

        await client.send(command);
      },

      async getDownloadUrl(storageUri) {
        const url = new URL(storageUri);
        const bucket = url.host;
        const key = url.pathname.slice(1);

        const command = new GetObjectCommand({ Bucket: bucket, Key: key });
        const signedUrl = await getSignedUrl(client, command, {
          expiresIn: 3600,
        });

        return { fileUrl: signedUrl };
      },
    };
  },
});

Helper Utilities

The @hot-updater/plugin-core package provides helper functions:

parseStorageUri

Parses storage URIs into bucket and key components:

const { bucket, key } = parseStorageUri(
  "s3://my-bucket/path/file.bundle",
  "s3"
);
// bucket: "my-bucket"
// key: "path/file.bundle"

getContentType

Returns MIME type based on file extension:

const contentType = getContentType("bundle.js");
// Result: "application/javascript"

CLI Configuration

Use your custom plugin in hot-updater.config.ts:

import { defineConfig } from "@hot-updater/core";
import { customStorage } from "./customStorage";

export default defineConfig({
  storage: customStorage({
    region: "us-east-1",
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    },
    bucketName: process.env.BUCKET_NAME!,
  }),
  // ... other config
});

Custom Server Usage

Use your plugin with createHotUpdater for self-hosted servers:

import { createHotUpdater } from "@hot-updater/core";
import { customStorage } from "./customStorage";

const hotUpdater = createHotUpdater({
  storages: [
    customStorage({
      region: "us-east-1",
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
      },
      bucketName: process.env.BUCKET_NAME!,
    }),
  ],
  // ... other options
});

Security

  • Never hardcode credentials in plugin code
  • Use environment variables for sensitive data
  • Validate storage URIs before processing
  • Implement proper authentication headers

Storage URI Format

Use a consistent URI format: protocol://bucket/path/to/file

return {
  storageUri: `${supportedProtocol}://${bucketName}/${storageKey}`,
};