Admin API

Plugins

Plugins extend HappyView with WebAssembly modules sourced from the official plugin registry or any URL serving a manifest.json. Most endpoints take a plugin manifest URL and load (or reload) the plugin in place — no restart needed. Encrypted plugin secrets require TOKEN_ENCRYPTION_KEY to be configured.

const TOKEN = "hv_..."; // your API key
const headers = { Authorization: `Bearer ${TOKEN}` };

List installed plugins

GET /admin/plugins

Requires plugins:read. Returns every loaded plugin with its source, required secrets, configuration status, and any pending updates from the official registry cache.

interface RequiredSecret {
  key: string;
  name: string;
  description: string;
}

interface PluginSummary {
  id: string;
  name: string;
  version: string;
  source: string;
  url: string;
  sha256: string | null;
  enabled: boolean;
  auth_type: string;
  required_secrets: RequiredSecret[];
  secrets_configured: boolean;
  loaded_at: string | null;
  update_available: boolean;
  latest_version: string;
  pending_releases: string[];
}

interface PluginsResponse {
  encryption_configured: boolean;
  plugins: PluginSummary[];
}

const response = await fetch("http://127.0.0.1:3000/admin/plugins", {
  headers,
});
const data: PluginsResponse = await response.json();

Response: 200 OK

{
  "encryption_configured": true,
  "plugins": [
    {
      "id": "steam",
      "name": "Steam",
      "version": "1.2.0",
      "source": "url",
      "url": "https://example.com/plugins/steam/manifest.json",
      "sha256": null,
      "enabled": true,
      "auth_type": "openid",
      "required_secrets": [
        {
          "key": "PLUGIN_STEAM_API_KEY",
          "name": "Steam Web API Key",
          "description": "Get your API key at steamcommunity.com/dev/apikey"
        }
      ],
      "secrets_configured": true,
      "loaded_at": null,
      "update_available": false,
      "latest_version": "1.2.0",
      "pending_releases": []
    }
  ]
}

secrets_configured is true if the plugin has no required secrets, or if a row exists for it in plugin_configs. update_available and pending_releases are populated from the cached official registry — call POST /admin/plugins/{id}/check-update to refresh them.

Preview a plugin before installing

POST /admin/plugins/preview

Requires plugins:create. Fetches and parses a manifest without installing the plugin, so the dashboard can show what it would register.

interface PluginPreview {
  id: string;
  name: string;
  version: string;
  description: string;
  icon_url: string;
  auth_type: string;
  required_secrets: RequiredSecret[];
  manifest_url: string;
  wasm_url: string;
}

const response = await fetch("http://127.0.0.1:3000/admin/plugins/preview", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/plugins/steam/manifest.json",
  }),
});
const data: PluginPreview = await response.json();

Response: 200 OK

{
  "id": "steam",
  "name": "Steam",
  "version": "1.2.0",
  "description": "Import your Steam game library and playtime data.",
  "icon_url": "https://example.com/steam-icon.png",
  "auth_type": "openid",
  "required_secrets": [
    { "key": "PLUGIN_STEAM_API_KEY", "name": "Steam Web API Key", "description": "..." }
  ],
  "manifest_url": "https://example.com/plugins/steam/manifest.json",
  "wasm_url": "https://example.com/plugins/steam/steam.wasm"
}

Returns 400 Bad Request if the manifest can't be fetched or parsed.

Install a plugin

POST /admin/plugins

Requires plugins:create. Fetches the manifest, downloads the WASM, registers the plugin, and persists it.

const response = await fetch("http://127.0.0.1:3000/admin/plugins", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/plugins/steam/manifest.json",
    sha256: "abc123...",
  }),
});
const data: PluginSummary = await response.json();
FieldTypeRequiredDescription
urlstringyesURL to the plugin's manifest.json
sha256stringnoOptional sha256 of the WASM binary. If provided, install fails when the downloaded hash mismatches

Response: 200 OK returning the same PluginSummary shape as the list endpoint. secrets_configured will be false if the plugin requires any secrets — call PUT /admin/plugins/{id}/secrets to configure them before the plugin can run.

List official plugins

GET /admin/plugins/official

Requires plugins:read. Returns the cached catalog of plugins from the official registry. The cache is refreshed periodically by the server; use POST /admin/plugins/{id}/check-update to force-refresh a single entry.

Response: 200 OK

{
  "last_refreshed_at": "2026-04-13T11:00:00Z",
  "plugins": [
    {
      "id": "steam",
      "name": "Steam",
      "description": "Import your Steam game library and playtime data.",
      "icon_url": "https://example.com/steam-icon.png",
      "latest_version": "1.2.0",
      "manifest_url": "https://example.com/plugins/steam/manifest.json"
    }
  ]
}

Remove a plugin

DELETE /admin/plugins/{id}

Requires plugins:delete. Unregisters the plugin from the runtime and deletes its row from the plugins table. Secrets stay in plugin_configs, so they're reused if you reinstall.

Response: 204 No Content. Returns 404 Not Found if no plugin with that id is loaded.

Reload a plugin

POST /admin/plugins/{id}/reload

Requires plugins:create. Re-fetches the plugin from its current source URL and re-registers it. Useful after publishing a new version of a plugin you host yourself.

The body is optional. To point the plugin at a new URL, pass:

{ "url": "https://example.com/plugins/steam/manifest.json" }

When a new URL is provided, the stored sha256 is cleared (the new version has its own hash). File-based plugins cannot be reloaded via this endpoint and return 400 Bad Request.

Response: 200 OK with the refreshed PluginSummary.

Check for plugin updates

POST /admin/plugins/{id}/check-update

Requires plugins:create. Forces a cache refresh for one plugin from the official registry, then returns the updated PluginSummary with update_available, latest_version, and pending_releases reflecting the latest catalog state.

Response: 200 OK with a PluginSummary.

Get plugin secrets

GET /admin/plugins/{id}/secrets

Requires plugins:read. Returns the plugin's configured secrets with values masked (last 4 characters shown for values longer than 8 characters, otherwise fully masked). Requires TOKEN_ENCRYPTION_KEY to be configured.

Response: 200 OK

{
  "plugin_id": "steam",
  "secrets": {
    "PLUGIN_STEAM_API_KEY": "********ABCD"
  }
}

Update plugin secrets

PUT /admin/plugins/{id}/secrets

Requires plugins:create. Encrypts the provided secret values with TOKEN_ENCRYPTION_KEY (AES-256-GCM) and upserts them into plugin_configs.

const response = await fetch("http://127.0.0.1:3000/admin/plugins/steam/secrets", {
  method: "PUT",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    secrets: {
      PLUGIN_STEAM_API_KEY: "your-new-api-key",
    },
  }),
});

Special handling:

  • Values starting with ******** are treated as masked placeholders and the existing encrypted value is preserved (so you can GET then PUT without re-typing every secret).
  • Empty string values are not stored — use them to clear a secret.

Response: 204 No Content