HotupdaterHot Updater
Database Adapters

Supabase Database

Run Hot Updater's database on Supabase PostgreSQL.

Installation

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

An Edge Function that composes createHotUpdater, as in Edge runtime usage, depends on @hot-updater/server as well as @hot-updater/supabase.

Setup

Set up the backend with an AI agent or the interactive CLI, then use the configuration below. Managed setup applies the migration for you.

The settings below connect to an initialized database; they do not create its tables or RPC function, and hot-updater db migrate does not run for Supabase. To prepare a project yourself, copy 0001_hot-updater_1.0.0.sql from the installed @hot-updater/supabase package's supabase/migrations directory into your project's supabase/migrations directory, then apply it with the Supabase CLI linked to that project:

npx supabase db push

The migration creates the tables of core and of the managed Edge Function's plugins, Insights, API keys, and Remote Config, under the hot_updater_v1_ prefix, with row-level security on every table, the write guard table, and the hot_updater_v1_apply function, then writes the schema settings rows last.

An Edge Function that runs other plugins also needs their tables, and the apply function must be allowed to reach them. hot-updater db generate writes a migration that does both to supabase/migrations/<timestamp>_hot-updater.sql; apply it with npx supabase db push. It reads a Node module that passes the function's plugins to createHotUpdater with supabaseDatabase from @hot-updater/supabase.

Schema settings

Before its first read or write, the database checks the settings rows schema.engine and schema.core in one batch read, and the server checks schema.<id> for each plugin it runs, such as schema.insights and schema.apiKeys, in another. While one is missing or different, or PostgREST cannot find the apply function, it throws HotUpdaterSchemaMigrationRequiredError, and the server's handlers answer 503. A running server checks again on its next request, so it recovers once the migration has run.

hot-updater init stops when the project already holds an incompatible Hot Updater schema.

How the database is reached

supabaseDatabase(config) returns an engine database, { name, adapter }, which defineConfig and createHotUpdater both accept. Its adapter is Hot Updater's shared SQL core for PostgreSQL, behind the schema settings check.

Every read and write calls one function with the service role key: hot_updater_v1_apply(p_statements jsonb). It runs the SQL core's statements in the caller's transaction, so a write, sent as one call, commits atomically. The function runs as SECURITY INVOKER with a fixed search_path, and only service_role may execute it. It accepts only the SQL core's statement shapes on Hot Updater's tables, and every value arrives in its jsonb parameter, never in the SQL text.

Rows past their tables' retention, such as Insights events and rollups, need no schedule or pg_cron. The Edge Function request whose write takes the hourly lease first deletes a bounded batch of them through the apply RPC.

Configuration

interface SupabaseDatabaseConfig {
  supabaseUrl: string; // Your Supabase project URL
  supabaseServiceRoleKey: string; // Your Supabase service role or secret key
}

Usage

These CLI examples load the HOT_UPDATER_SUPABASE_* variables written to .env.hotupdater by managed setup. Keep that file's existing values.

hot-updater.config.ts (excerpt)
import { existsSync } from "node:fs";
import { supabaseDatabase } from "@hot-updater/supabase"; 
import { defineConfig } from "hot-updater";

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

export default defineConfig({
  // ... build, storage, plugins, and updateStrategy
  database: supabaseDatabase({ 
    supabaseUrl: process.env.HOT_UPDATER_SUPABASE_URL!, 
    supabaseServiceRoleKey: process.env.HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY!, 
  }), 
});

Edge runtime usage

Use the /edge subpath when composing createHotUpdater inside a Supabase Edge Function. The provider keeps the same name while the subpath makes the runtime boundary explicit:

src/hotUpdater.ts (excerpt)
import { createHotUpdater } from "@hot-updater/server";
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
import { supabaseDatabase } from "@hot-updater/supabase/edge"; 

export const hotUpdater = createHotUpdater({
  database: supabaseDatabase({ 
    supabaseUrl: Deno.env.get("SUPABASE_URL")!, 
    supabaseServiceRoleKey: Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, 
  }), 
  storage, // The adapter hot-updater.config.ts uploads with, such as supabaseStorage(...).
  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 Edge Function runs the same list. List the server's plugins in hot-updater.config.ts too; see CLI Configuration.

Complete example

The hot-updater.config.ts that hot-updater init writes for managed Supabase, with Storage, the Postgres database, and the plugins the managed Edge Function runs:

hot-updater.config.ts
import { bare } from "@hot-updater/bare";
import { supabaseDatabase, supabaseStorage } from "@hot-updater/supabase";
import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
import { defineConfig } from "hot-updater";
import { existsSync } from "node:fs";

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

export default defineConfig({
  build: bare({ enableHermes: true }),
  storage: supabaseStorage({
    supabaseUrl: process.env.HOT_UPDATER_SUPABASE_URL!,
    supabaseServiceRoleKey: process.env.HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY!,
    bucketName: process.env.HOT_UPDATER_SUPABASE_BUCKET_NAME!,
  }),
  database: supabaseDatabase({
    supabaseUrl: process.env.HOT_UPDATER_SUPABASE_URL!,
    supabaseServiceRoleKey: process.env.HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY!,
  }),
  plugins: [apiKeys(), insights(), remoteConfig()],
  updateStrategy: "appVersion", // or "fingerprint"
});

On this page