SecureBin Developer Platform

SecureBin is evolving from a secure USB product into an offline-first sovereign computing platform. The goal is to let developers build privacy-focused, portable applications that run inside an encrypted vault environment with clear packaging, security boundaries, and future monetization paths.

Brain Icon
Offline-first platform
Applications are designed to run locally with encrypted storage, strong runtime isolation, and minimal reliance on external cloud services.
Security Icon
Sovereign security model
SecureBin centers around vault encryption, permission-scoped app behavior, and hardware-bound digital ownership.
Package Icon
Simple packaging
Apps can evolve from single HTML files into a formal package model with manifest metadata, assets, and permission declarations.
Developer Economy Icon
Developer economy
The long-term vision includes paid apps, premium tools, privacy infrastructure, AI utilities, and Bitcoin-native software.
Platform architecture
Hardware Layer
↓
SecureBin USB Vault
↓
SecureOS Runtime
↓
Sandbox Execution Engine
↓
SecureBin SDK
↓
Application
Security model
AreaGoalWhy it matters
Vault encryptionProtect data at restKeeps sensitive files and app state private inside the encrypted environment
Runtime isolationSeparate apps from the hostReduces cross-app interference and improves platform trust
Permission modelScope app capabilitiesLets users understand what an app can access before installing it
Offline-first executionWork without cloud dependenceAligns with SecureBin's sovereignty and privacy philosophy
No telemetry by defaultRespect user ownershipReinforces the brand promise of private computing
App package direction

Today, SecureBin apps can be single HTML files. Long term, they can move toward a formal package structure such as .sbapp, with a manifest and optional runtime assets.

my-app.sbapp manifest.json index.html icon.png assets/ runtime.js
Manifest example
{
  "name": "Bitcoin Node",
  "version": "1.0",
  "permissions": [
    "vault.read",
    "vault.write",
    "network.rpc"
  ],
  "entry": "index.html",
  "offline": true
}
Developer journey

A strong SecureBin developer flow should turn curiosity into app creation, publishing, and eventually monetization.

  1. Discover SecureBin

    Learn the philosophy, architecture, and app model through the docs.

  2. Install the SDK

    Start with templates, manifest examples, and vault-safe APIs.

  3. Build your first app

    Create a working SecureBin-compatible application and test it inside the vault environment.

  4. Package and publish

    Prepare the app for the store with iconography, metadata, and permission declarations.

  5. Monetize and iterate

    Ship paid tools, privacy utilities, offline AI features, or Bitcoin-native software.

Monetization opportunities
Bitcoin tools
Remote node clients, fee utilities, wallet companions, and privacy-focused transaction tools.
Offline AI Icon
Offline AI
Private model runners, local assistants, LoRA packs, and encrypted prompt workflows.
Vault Utilities Icon
Vault utilities
Password managers, note systems, file shredders, media tools, and secure productivity apps.
Enterprise Apps Icon
Enterprise secure apps
Internal tools built for portable workflows, protected storage, and restricted execution environments.

Overview

SecureBin apps are plain HTML files that run in their own Electron modal window inside the SecureBin application. They get access to a curated JavaScript API (window.usb) that lets them read and write data to the encrypted vault, respond to theme changes, and handle their own lifecycle cleanly.

Single-file HTML apps
Your entire app lives in one .html file — HTML, CSS, and JavaScript together. No bundler, no framework, no build step required.
Encrypted persistent storage
Data you write via appData is stored encrypted on disk inside the vault, and automatically backed up to localStorage as a fallback.
Automatic theming
SecureBin injects theme CSS variables into your app via postMessage. Your app stays visually in sync with the user's chosen theme.
No network calls
Apps run offline-first. All data is local and encrypted. No external APIs or CDNs — everything is bundled in your HTML file.

Quick Start

The fastest path to a working SecureBin app — copy this boilerplate, customize it, and drop it into the apps folder.

  1. Create your HTML file

    Name it something short with no spaces: my-app.html. This becomes your app's ID.

  2. Set up fonts

    SecureBin apps use IBM Plex Sans for UI text and IBM Plex Mono for labels, metadata, and code. Because apps must run offline, do not load fonts from Google Fonts — the request will fail when the device has no internet access, and apps that declare no internet permission will be blocked from making it anyway. Instead, rely on the system-font stack:
    font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
    font-family: 'IBM Plex Mono', ui-monospace, Menlo, Consolas, monospace;
    Users who have IBM Plex installed locally will see the correct typeface. All other users get a clean system font — which is fine. If your app declares internet permission and you want to load fonts when online, you may add a Google Fonts link as a progressive enhancement, but never rely on it.

  3. Add the CSS variable block

    Copy the :root { --bg: ... } block and @media (prefers-color-scheme: dark) fallback. This ensures your app looks good before the theme is injected.

  4. Add the theme injection script

    Copy the injectThemeVars and postMessage listener pattern. It takes about 20 lines and handles all theme sync automatically.

  5. Initialise your storage

    Call window.usb.appData.readJSON(APP_ID, 'state.json') on startup. Fall back to localStorage if the bridge isn't available.

  6. Install it

    Copy your .html file into the apps folder or use the in-app installer. SecureBin will detect it immediately.

Planning to call vault methods (not just appData)? If your app uses vaultSaveFromUrl, vaultList, or any other window.usb vault method, read the Iframe Bridge section before you write those calls. Apps opened as standalone windows have window.usb available directly, but apps embedded as iframes do not — and the vaultCall() helper pattern handles both cases transparently.
my-app.html — minimal boilerplate
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My App</title>
  <!-- 1. Declare permissions your app needs (see Permissions section) -->
  <meta name="app-permissions" content="">
  <!-- 2. Theme injection target -->
  <style id="sb-injected-theme"></style>
  <style>
    :root {
      --bg:      #f9fafb;
      --panel:   #ffffff;
      --text:    #111827;
      --muted:   #6b7280;
      --border:  #e5e7eb;
      --accent:  #2563eb;
      --btn-bg:  #ffffff;
      --btn-text: #111827;
      --btn-border: #e5e7eb;
      --shadow:  0 1px 6px rgba(0,0,0,.06);
    }
    @media (prefers-color-scheme: dark) {
      :root {
        --bg:      #000000;
        --panel:   #080808;
        --text:    #ffffff;
        --muted:   rgba(255,255,255,.50);
        --border:  rgba(255,255,255,.10);
        --accent:  #60a5fa;
        --btn-bg:  #080808;
        --btn-text: #ffffff;
        --btn-border: rgba(255,255,255,.16);
        --shadow:  0 1px 10px rgba(0,0,0,.7);
      }
    }
    html, body { background: var(--bg); color: var(--text); }
  </style>
</head>
<body>
  <!-- Your app markup here -->
  <script>
  (async () => {
    "use strict";
    const APP_ID     = "my-app";
    const STATE_FILE = "state.json";
    const LS_KEY     = "securebin.my-app.state.v1";

    async function loadState() {
      if (window.usb?.appData?.readJSON) {
        try {
          const data = await window.usb.appData.readJSON(APP_ID, STATE_FILE);
          if (data) return data;
        } catch (_) {}
      }
      try {
        const raw = localStorage.getItem(LS_KEY);
        return raw ? JSON.parse(raw) : null;
      } catch (_) { return null; }
    }

    async function saveState(state) {
      if (window.usb?.appData?.writeJSON) {
        try { await window.usb.appData.writeJSON(APP_ID, STATE_FILE, state); return; } catch (_) {}
      }
      // Vault unavailable — localStorage fallback only
      try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (_) {}
    }

    const state = (await loadState()) || { items: [] };
    // ... build your UI with state here
  })();
  </script>
</body>
</html>

App Structure

A SecureBin app is just a single HTML file, but you can optionally ship a PNG icon alongside it. SecureBin's app system automatically discovers and registers both.

SECUREBIN/ apps/ my-app.html ← your app entry point my-app.png ← optional icon (512×512 recommended) my-app.meta.json ← auto-generated by installer (includes dependencies) my-app/ ← dependency bundle folder (created by installer) chart.umd.min.js ← downloaded dependency other-lib.js ← additional dependencies .usb-vault/ apps_data/ my-app/ state.json ← your app's encrypted saved data
Naming rules

Your app's ID is the filename without the .html extension. It must be lowercase, use only letters, digits, hyphens, and underscores, and be unique across installed apps.

File / FolderRequiredPurpose
my-app.html✅ YesThe app entry point — HTML, CSS, and JS. Can reference dependency files via relative paths.
my-app.pngOptionalApp icon shown in the launcher. 512×512px PNG recommended.
my-app.meta.jsonAutoGenerated by the installer. Contains display name, version, and the dependencies array.
my-app/AutoDependency bundle folder. Created by the installer and populated with downloaded library files.

Permissions

SecureBin enforces a permission system at app launch. Your app must declare which capabilities it needs via a <meta> tag in its <head>. If the tag is missing or the user denies a required permission, the app will not open.

Declaring permissions

Add the app-permissions meta tag immediately after your <meta charset>. The content attribute is a comma-separated list of permission identifiers. An empty string means the app needs no special permissions.

<!-- No special permissions needed -->
<meta name="app-permissions" content="">

<!-- App needs internet access -->
<meta name="app-permissions" content="internet">

<!-- App needs internet and can save files to USB vault -->
<meta name="app-permissions" content="internet,usb_download">
Available permissions
PermissionWhat it allowsPrompt shown to user
internetThe app may make outbound network requests (fetch, WebSocket, etc.)"Connect to Internet" — if denied, the app is blocked from opening
usb_uploadThe app may read files from the USB vault into memory"Access files from USB"
usb_downloadThe app may write/save files to the USB vault"Save files to USB"
How the permission prompt works

When a user taps an app icon, SecureBin reads the app-permissions meta tag from the app's HTML before opening it. If any listed permission has not yet been granted, a prompt is shown. The user can allow or deny each permission individually. Denying internet blocks the app from opening entirely. Denying usb_upload or usb_download allows the app to open but those capabilities will be unavailable.

Permissions are saved per app and can be changed at any time through Settings → App Permissions.

Always include the tag — even if it's empty. An app with a missing app-permissions meta tag is treated as an unknown app and the user is prompted about internet access regardless. Including content="" explicitly signals that your app is offline-only and requires no capabilities, which results in a smoother install experience with no prompt at all.
Do not load external resources from apps without internet

Never include Google Fonts links, CDN scripts, or any external URL in an app that doesn't declare internet permission. The request will either fail silently or cause the permission system to block the app. All fonts and assets must be self-contained or loaded from system fonts.

Dependencies

Apps are single HTML files, but sometimes you need a third-party library — a charting engine, a cryptography utility, or a UI toolkit. SecureBin supports this through a dependency bundle system: the installer downloads each declared dependency and saves it into a subfolder next to your HTML, where your app can reference it with a simple relative path.

How it works

When the SecureBin installer processes your app, it reads the dependencies array from your app's meta.json. For each entry it downloads the file from the given URL and writes it to apps/<slug>/<dest>. Your HTML references the file with a relative path — Electron resolves it correctly because the app is loaded via loadFile().

SECUREBIN/apps/ bitcoin-node.html ← entry point bitcoin-node.png bitcoin-node.meta.json bitcoin-node/ ← bundle folder, created by installer chart.umd.min.js ← downloaded from jsDelivr CDN
Referencing a dependency from your HTML

Use the app slug as the folder name in your relative path. Because Electron loads your app from the apps/ directory, the path resolves exactly as written:

<!-- In bitcoin-node.html -->
<script src="bitcoin-node/chart.umd.min.js" defer></script>

<!-- Multiple deps -->
<script src="my-app/lodash.min.js"></script>
<link   rel="stylesheet" href="my-app/theme.css">
meta.json — declaring dependencies

Add a dependencies array to your app's meta.json. Each entry has a url (where to download the file from) and a dest (the filename to save it as inside the bundle folder). The dest must be a plain filename with no slashes.

{
  "id":          "bitcoin-node",
  "name":        "Bitcoin Node",
  "version":     "2.0.0",
  "sourceUrl":   "https://www.us-securebin.com/assets/files/apps/bitcoin-node.html",
  "dependencies": [
    {
      "url":  "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js",
      "dest": "chart.umd.min.js"
    },
    {
      "url":  "https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js",
      "dest": "lodash.min.js"
    }
  ]
}
Declaring dependencies via the App Store upload form

If you submit your app through the SecureBin App Store, use the Dependencies card at the bottom of the upload form. Enter the CDN URL and an optional filename for each library. The store stores them and the installer reads them automatically on install — you don't touch meta.json manually.

Declaring dependencies via the app HTML

Embed your app identity, version, and dependencies directly in <meta> tags. The installer reads these automatically — this is the recommended approach for apps installed via Import App or from a URL, because no separate meta.json file is needed. The app-dependencies tag takes a JSON array of { url, dest } objects, identical to the meta.json format:

<meta name="app-id"          content="bitcoin-node">
<meta name="app-name"        content="Bitcoin Node">
<meta name="app-version"     content="2.0.0">
<meta name="app-permissions" content="internet">
<meta name="app-dependencies" content='[{"url":"https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js","dest":"chart.umd.min.js"}]'>

<!-- Dependency loaded from the bundle folder -->
<script src="bitcoin-node/chart.umd.min.js" defer></script>
Always write a graceful fallback

A dependency file could be missing if the user installed manually without running the installer, or if the CDN was unreachable at install time. Guard your code with a typeof check and fall back to a built-in solution where possible. This way your app is always functional, even without the bundle:

// Wait a moment for the deferred script to execute, then check
await new Promise(r => setTimeout(r, 50));

if (typeof Chart !== "undefined") {
  // Chart.js loaded from bundle — use canvas chart
  initChartJs();
} else {
  // Fallback to built-in SVG chart
  initSvgChart();
}
Installing dependencies via Import App

If you're running SecureBin as a packaged executable, use the Import App button in the Apps panel to install a local HTML file. As long as your app declares its dependencies via the app-dependencies meta tag, the importer automatically downloads each dependency and creates the bundle folder — no manual file copying required:

  1. Open the Apps panel and click Import App
  2. Select your .html file — the importer reads the app-dependencies meta tag
  3. Optionally select an icon image
  4. SecureBin downloads each declared dependency into apps/<slug>/ automatically

The app appears in the Imported Apps section of the Apps panel. If you need to supply dependencies manually (e.g. for an air-gapped install), place the files directly into the bundle folder alongside the HTML:

SECUREBIN\apps\bitcoin-node.html
SECUREBIN\apps\bitcoin-node.png
SECUREBIN\apps\bitcoin-node\chart.umd.min.js  ← download from jsDelivr
Rules and restrictions
RuleDetails
URLs must be https://Plain http:// and relative URLs are rejected by the installer
dest is a filename onlyNo slashes, no path traversal. chart.min.js ✅ — ../chart.js
Dependencies are unencryptedThey live in apps/<slug>/, outside the vault. Never put sensitive data in a dep file.
No vault unlock requiredApps and their dependencies load without requiring the vault to be unlocked. Only appData (user state) is encrypted and vault-gated.
Self-destruct safeThe vault self-destruct wipes .usb-vault/ only. App files and bundles in apps/ are not touched.
Keep apps self-contained where possible. Dependencies add install complexity and require an internet connection at install time. For small utilities — a date formatter, a UUID generator — just inline the code in your HTML. Reserve the dependency system for genuinely heavy libraries (charting engines, crypto primitives, WASM runtimes) where bundling inline would be impractical.

App Storage

SecureBin gives each app its own key-value store via the appData API. Data persists across sessions and survives app restarts. When running outside SecureBin (e.g. in a plain browser during development), a localStorage fallback keeps things working seamlessly.

appData is not encrypted at rest. Files written via appData.writeJSON or appData.writeText are stored as plain JSON in .usb-vault/apps_data/ on disk. Do not store secrets, keys, or sensitive user data through this API — use the encrypted vault file APIs instead.
The vault-primary storage pattern

The vault is the authoritative store. localStorage is a fallback for development in a plain browser — it should only be written when the vault bridge is unavailable, not alongside every vault write. Writing localStorage unconditionally leaves stale copies of sensitive data in the browser profile even after the USB is removed.


async function loadState() {
  if (window.usb?.appData?.readJSON) {
    try {
      const data = await window.usb.appData.readJSON(APP_ID, "state.json");
      if (data !== null) return data;
    } catch (_) {}
  }
  // Fallback: plain localStorage (development / no vault)
  try {
    const raw = localStorage.getItem(LS_KEY);
    return raw ? JSON.parse(raw) : null;
  } catch (_) { return null; }
}

// Write — vault first. Only fall back to localStorage if vault is unavailable.
async function saveState(data) {
  if (window.usb?.appData?.writeJSON) {
    try {
      await window.usb.appData.writeJSON(APP_ID, "state.json", data);
      return; // vault succeeded — do NOT write localStorage
    } catch (_) {}
  }
  // Vault unavailable — use localStorage as fallback only
  try { localStorage.setItem(LS_KEY, JSON.stringify(data)); } catch (_) {}
}
Multiple data files

You can store multiple JSON files for one app. Use meaningful filenames to separate concerns:

await window.usb.appData.writeJSON(APP_ID, "settings.json", { theme: "dark" });
await window.usb.appData.writeJSON(APP_ID, "items.json",    { list: [...] });
await window.usb.appData.writeJSON(APP_ID, "history.json",  { log: [...] });
// Each stored at: .usb-vault/apps_data/my-app/{filename}
Text files

For non-JSON data (markdown notes, CSV exports, raw text), use readText / writeText:

await window.usb.appData.writeText(APP_ID, "notes.md", "# My Notes\n\nHello!");
const text = await window.usb.appData.readText(APP_ID, "notes.md");
Serialisation queuing

If you save frequently (e.g. on every keystroke), avoid race conditions by chaining your writes on a promise queue:

let saveChain = Promise.resolve();

function queueSave(data) {
  const snapshot = JSON.parse(JSON.stringify(data)); // deep clone
  saveChain = saveChain
    .catch(() => {})
    .then(() => saveState(snapshot));
  return saveChain;
}

Theming

SecureBin pushes its active theme to your app via the postMessage API. By using the provided CSS variables everywhere in your styles, your app automatically matches the user's chosen theme.

How it works

When your app opens, SecureBin sends a SB_THEME message containing a vars object of CSS custom properties. You inject these into a dedicated <style> tag on the html and body selectors so they override your defaults.

// 1. Reserve a style tag for theme injection in your <head>
const themeStyle = document.getElementById("sb-injected-theme");

// 2. Inject vars from any source
function injectThemeVars(vars) {
  const css = Object.entries(vars)
    .filter(([k]) => k.startsWith("--"))
    .map(([k, v]) => `${k}:${v}`)
    .join(";");
  themeStyle.textContent = `:root,html,body{${css}}`;
}

// 3. Listen for live theme changes
window.addEventListener("message", (e) => {
  if (e.data?.type === "SB_THEME" && e.data.vars) {
    injectThemeVars(e.data.vars);
  }
});

// 4. Request the current theme immediately on load
try {
  if (window.parent !== window) {
    window.parent.postMessage({ type: "SB_REQUEST_THEME" }, "*");
  }
} catch (_) {}
Available CSS Variables

Use only these variables in your app's styles. Never hardcode colours — the theme system will handle everything.

VariableLight valueDark valueUsage
--bg#f9fafb#000000Page background
--panel#ffffff#080808Card / panel backgrounds
--panel-2#f9fafb#0d0d0dNested / secondary panels
--text#111827#ffffffPrimary text colour
--muted#6b7280rgba(255,255,255,.50)Secondary / helper text
--border#e5e7ebrgba(255,255,255,.10)Borders, dividers
--border-2#e5e7ebrgba(255,255,255,.06)Subtler borders, table rows
--accent#2563eb#60a5faButtons, links, highlights
--btn-bg#ffffff#080808Default button background
--btn-text#111827#ffffffButton label text
--btn-border#e5e7ebrgba(255,255,255,.16)Button border colour
--shadow0 1px 6px…0 4px 24px…Box shadow values
--progress#3b82f6#22c55eProgress bars
--success#16a34a#22c55eSuccess states, confirmations
--danger#dc2626#ef4444Errors, destructive actions
--warn#d97706#f59e0bWarnings, caution states

Lifecycle Events

SecureBin communicates with your app through window.postMessage events. Hook into these to handle uninstallation, theme changes, and graceful shutdown.

Message typeDirectionPayloadWhen to use
SB_THEMESecureBin → App{ type, vars }Respond to live theme changes
SB_REQUEST_THEMEApp → SecureBin{ type }Ask for current theme on load
SB_APP_UNINSTALLEDSecureBin → App{ type, appId }Clean up localStorage on uninstall
vault:changePinProgressSystem internal{ phase, done, total, failed }System event — not available to app developers. Sent by SecureBinOS to the internal renderer only during vault PIN re-encryption.
Uninstall cleanup

When the user uninstalls your app, SecureBin dispatches SB_APP_UNINSTALLED. You should clear your localStorage backup to avoid leaving stale data behind.

let uninstalling = false;

window.addEventListener("message", (e) => {
  if (e.data?.type === "SB_APP_UNINSTALLED" && e.data.appId === APP_ID) {
    uninstalling = true;
    localStorage.removeItem(LS_KEY);
  }
});

// On close, write a final backup — only if vault is unavailable
window.addEventListener("pagehide", () => {
  if (uninstalling) { localStorage.removeItem(LS_KEY); return; }
  if (window.usb?.appData) return; // vault handles persistence
  localStorage.setItem(LS_KEY, JSON.stringify(appState));
});

Iframe Bridge Pattern

SecureBin app windows created via sb:openInstalledApp receive the Electron preload, so window.usb is available directly. However, apps embedded as <iframe> elements inside renderer.html — such as Pastes — do not receive the preload. In those cases window.usb is undefined and all vault calls must go through the SB_BRIDGE_CALL postMessage proxy.

How the bridge works

The iframe sends a SB_BRIDGE_CALL message to its parent (renderer.html). The renderer checks the method against its BRIDGE_ALLOWED whitelist, calls window.usb[method](...args) on the renderer's own preload, and sends the result back as SB_BRIDGE_RESULT. The existing __SB_AUTH_BRIDGE__.callParent() helper handles all of this automatically.

Methods allowed through the bridge

Only explicitly whitelisted methods can be proxied. As of this version, the whitelist in renderer.html includes:

const BRIDGE_ALLOWED = new Set([
  // Vault file operations
  'vaultList', 'vaultShred', 'vaultDelete', 'vaultRead', 'vaultMediaToken',
  // Folder operations
  'folderList',
  // Auth & status
  'vaultStatus', 'vaultLock', 'getVaultInfo', 'vaultGetInfo',
  // Downloads
  'vaultSaveFromUrl', 'vaultGetRoot', 'vaultSaveFromURL', 'vaultSaveBytes',
  // App management
  'appsList', 'appsOpen', 'appsUninstall', 'appsInstall', 'appsInstallFromUrl',
  // appData — handled via separate appDataMethods mapping
  'appDataReadJSON', 'appDataWriteJSON', 'appDataReadText', 'appDataWriteText'
]);
Recommended pattern — vaultCall helper

Rather than branching on whether window.usb exists, use a single vaultCall() helper that transparently routes to the correct path. This makes your app work correctly whether it is opened as a standalone window or embedded as an iframe.

const usb = window.usb || window.securebin;

async function vaultCall(method, ...args) {
  // Direct: standalone BrowserWindow with preload
  if (usb && typeof usb[method] === 'function') {
    return await usb[method](...args);
  }
  // Proxy: iframe inside renderer.html — postMessage to parent
  if (window.__SB_AUTH_BRIDGE__?.callParent) {
    return await window.__SB_AUTH_BRIDGE__.callParent(method, args);
  }
  throw new Error(`vaultCall: '${method}' not reachable`);
}

// Usage — works in both contexts
const r = await vaultCall('vaultSaveFromUrl', saveUrl, filename, mime);
if (r.ok) console.log('Saved to vault Downloads');

window.usb Reference

The window.usb object (also aliased as window.securebin) is the bridge between your app and SecureBin's main process. It is injected by the preload script into every app window.

App management
usb.appsList() → Promise<{ ok, apps[], error }>

Returns an array of all installed apps with their metadata.

const { apps } = await window.usb.appsList();
// apps: [{ id, name, version, icon, iconUrl, file, fullPath, source, importedAt, installedAt }]
usb.appsOpen(id) (appId: string)

Opens another installed app by its ID in a new modal window. Useful for building launcher-style apps.

await window.usb.appsOpen("calculator");
usb.appsInstallFromUrl(id, url, iconUrl, name, version, dependencies?)

Downloads and installs an app from a URL. The HTML file is fetched, saved into the apps directory, and registered.

await window.usb.appsInstallFromUrl(
  "my-tool",
  "https://example.com/apps/my-tool.html",
  "https://example.com/apps/my-tool.png",
  "My Tool",
  "1.0.0"
);
usb.appsInstall(options) (options: { id, htmlContent, iconUrl?, name?, version? })

Installs an app from in-memory HTML content (no URL fetch). Used internally by the installer when the HTML has already been downloaded or constructed. Prefer appsInstallFromUrl for remote installation; use appsInstall when you already have the HTML string in hand.

await window.usb.appsInstall({
  id:          "my-tool",
  htmlContent: htmlString,   // already-fetched HTML
  iconUrl:     "https://example.com/icon.png",  // optional
  name:        "My Tool",
  version:     "1.0.0"
});
usb.appsUninstall(id) (appId: string | { id, removeData? })

Uninstalls an app. Pass { id, removeData: true } to also delete the app's stored data.

// Uninstall only (keep data)
await window.usb.appsUninstall("my-tool");

// Uninstall and wipe data
await window.usb.appsUninstall({ id: "my-tool", removeData: true });
Vault utilities
usb.vaultSaveFromUrl(url, filename, mime) → Promise<{ ok, filename, vaultUrl, relPath, absPath, folderRel }>

The primary download method. Accepts an https: URL or a data: URI (base64-encoded text/binary). Automatically finds or creates the vault Downloads folder, fetches the content, encrypts it, and saves it. This is the recommended single-call path for all file and paste downloads.

// Save a remote file
const r = await window.usb.vaultSaveFromUrl(
  "https://example.com/report.pdf",
  "report.pdf",
  "application/pdf"
);

// Save in-memory text as a data: URI
const b64 = btoa(unescape(encodeURIComponent(content)));
const r = await window.usb.vaultSaveFromUrl(
  `data:text/plain;base64,${b64}`,
  "notes.txt",
  "text/plain"
);
if (r.ok) console.log('Saved to', r.relPath);
usb.vaultGetRoot() → Promise<{ ok, vaultDir, downloadsRel, downloadsAbs }>

Resolves the vault's root directory path and ensures the Downloads folder exists inside it.

const root = await window.usb.vaultGetRoot();
// root.vaultDir     — absolute path to the .usb-vault directory
// root.downloadsRel — vault-relative path of the Downloads folder
// root.downloadsAbs — absolute path of the Downloads folder
if (root.ok) console.log(root.downloadsRel);
usb.vaultSaveFromURL(payload) → Promise<{ ok, absPath, rel, vaultUrl }>

Saves a URL or data: URI to an explicit vault-relative path of your choosing.

const root = await window.usb.vaultGetRoot();
const r = await window.usb.vaultSaveFromURL({
  url:  `data:text/plain;base64,${b64}`,
  name: "notes.txt",
  rel:  root.downloadsRel,
  mime: "text/plain"
});
usb.vaultSaveBytes(payload) → Promise<{ ok, absPath, rel, vaultUrl }>

Saves raw bytes (an array of integers, Uint8Array, or Buffer) directly into the vault without any network fetch. The bytes are encrypted and written as a .uvf file.

const root = await window.usb.vaultGetRoot();
const bytes = Array.from(new TextEncoder().encode(myText));
const r = await window.usb.vaultSaveBytes({
  name: "export.txt",
  rel:  root.downloadsRel,
  mime: "text/plain",
  data: bytes
});
usb.ping() → Promise<any>

Health check — confirms the IPC bridge is alive. Resolves if SecureBin is running, rejects otherwise.

const inSecureBin = await window.usb.ping().then(() => true).catch(() => false);

appData Reference

All methods live under window.usb.appData. They all return Promises and have automatic filesystem fallback built in — so they work in browser dev mode too.

appData.readJSON(appId, fileName) → Promise<any | null>

Reads a JSON file from the app's data directory. Returns the parsed object, or null if the file doesn't exist yet.

const data = await usb.appData.readJSON("my-app", "state.json");
// data: { items: [...] } or null
appData.writeJSON(appId, fileName, data) → Promise<true>

Serialises data to JSON and writes it to the app's data directory. Creates the directory if needed.

await usb.appData.writeJSON("my-app", "state.json", { items: ["a", "b"] });
appData.readText(appId, fileName) → Promise<string | null>

Reads a file as raw UTF-8 text. Returns the string, or null if not found.

const md = await usb.appData.readText("my-app", "notes.md");
appData.writeText(appId, fileName, text, mime?) → Promise<true>

Writes a string to a file. The optional mime parameter is informational only.

await usb.appData.writeText("my-app", "export.csv", "name,age\nAlice,30");
appData.deleteFile(appId, fileName) → Promise<boolean>

Deletes a file from the app's data directory. Returns true if deleted, false if it didn't exist.

await usb.appData.deleteFile("my-app", "old-data.json");
appData.deleteFile is not available through the iframe bridge. If your app runs as an iframe (e.g. embedded in renderer.html), calls to appData.deleteFile will silently fail because appDataDeleteFile is not included in the BRIDGE_ALLOWED whitelist. Use the standalone window context (opened via appsOpen) if you need to delete app data files.

CSS Variables

Build your entire UI using only these CSS variables. The theme system will overwrite them at runtime, keeping your app in sync with the user's SecureBin theme automatically.

Recommended button styles

Copy these base button classes — they mirror what SecureBin's own UI uses and will feel native:

.btn {
  height: 36px;
  padding: 0 12px;
  border-radius: 999px;
  border: 1px solid var(--btn-border);
  background: var(--btn-bg);
  color: var(--btn-text);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: opacity .15s ease, transform .15s ease;
}
.btn:hover    { opacity: .82; }
.btn:active   { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: default; }

.btn.primary  { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.ghost    { background: transparent; }
Card pattern
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 16px;
  box-shadow: var(--shadow);
}

Packaging & Icons

Your app ships as one or two files. No build step, no bundling — just copy them into the right place.

App icon

Place a PNG named my-app.png alongside your HTML file in the apps folder. SecureBin uses this in the launcher grid.

  • Format: PNG with alpha transparency
  • Recommended size: 512 × 512 pixels
  • Keep it simple — it will be shown at 40–80px in most contexts
  • If no icon is present, SecureBin shows a generic puzzle piece icon
meta.json (optional)

When installing via URL, SecureBin auto-generates a .meta.json. You can also create one manually to provide display metadata and declare dependencies:

{
  "id":           "my-app",
  "name":         "My App",
  "version":      "1.0.0",
  "description":  "A helpful tool for SecureBin.",
  "icon":         "my-app.png",
  "dependencies": []
}

If your app uses third-party libraries, populate the dependencies array. See the Dependencies section for the full format.

Installation

Method 1 — Import App (recommended for packaged installs)

Use the Import App button in the Apps panel to install a local HTML file directly from your filesystem. If your app declares dependencies via the app-dependencies meta tag, they are downloaded automatically. The app appears in the Imported Apps section once added.

For air-gapped installs where no internet connection is available, place files directly into the SECUREBIN\apps\ folder on disk:

SECUREBIN\apps\my-app.html
SECUREBIN\apps\my-app.png
SECUREBIN\apps\my-app\chart.umd.min.js  ← download from your dep's CDN URL

SecureBin detects new apps immediately. No restart required.

Method 2 — Remote install via URL (with dependencies)

Use appsInstallFromUrl from another app or the in-app installer. Pass a dependencies array as the sixth argument — the installer downloads each file into apps/<slug>/ automatically:

await window.usb.appsInstallFromUrl(
  "bitcoin-node",                                        // app ID / slug
  "https://example.com/apps/bitcoin-node.html",          // HTML URL
  "https://example.com/apps/bitcoin-node.png",           // icon URL
  "Bitcoin Node",                                        // display name
  "2.0.0",                                               // version
  [                                                        // dependencies
    {
      url:  "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js",
      dest: "chart.umd.min.js"
    }
  ]
);

If you omit the dependencies argument the call behaves exactly as before — no bundle folder is created.

Method 3 — App Store

Submit your app at upload-app. Use the Dependencies card to declare your library URLs. When a user installs from the store, the installer reads the dependency list from meta.json on your CDN and downloads everything automatically — the user sees a single install tap with no extra steps.

Full Example — Notes App

A complete, minimal Notes app demonstrating all the patterns: dual storage, theme injection, lifecycle cleanup, and the CSS variable system.

notes.html — complete working app
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Notes</title>
  <style id="sb-injected-theme"></style>
  <style>
    :root{
      --bg:#f9fafb;--panel:#fff;--text:#111827;--muted:#6b7280;
      --border:#e5e7eb;--accent:#2563eb;--btn-bg:#fff;
      --btn-text:#111827;--btn-border:#e5e7eb;
    }
    @media (prefers-color-scheme:dark){
      :root{
        --bg:#000;--panel:#080808;--text:#fff;--muted:rgba(255,255,255,.50);
        --border:rgba(255,255,255,.10);--accent:#60a5fa;--btn-bg:#080808;
        --btn-text:#fff;--btn-border:rgba(255,255,255,.16);
      }
    }
    *{ box-sizing:border-box; font-family:system-ui,sans-serif; }
    html,body{ margin:0; height:100%; background:var(--bg); color:var(--text); }
    body{ display:flex; flex-direction:column; padding:20px; gap:14px; }
    h1{ font-size:18px; font-weight:700; color:var(--text); }
    textarea{
      flex:1; background:var(--panel); color:var(--text);
      border:1px solid var(--border); border-radius:10px;
      padding:14px; font-size:14px; line-height:1.6; resize:none; outline:none;
    }
    .row{ display:flex; gap:10px; align-items:center; }
    .btn{
      height:36px; padding:0 14px; border-radius:10px;
      border:1px solid var(--btn-border); background:var(--btn-bg);
      color:var(--btn-text); font-weight:600; font-size:13px; cursor:pointer;
    }
    .btn.primary{ background:var(--accent); border-color:var(--accent); color:#fff; }
    .saved{ font-size:12px; color:var(--muted); opacity:0; transition:opacity .4s; }
    .saved.show{ opacity:1; }
  </style>
</head>
<body>
  <div class="row">
    <h1>📝 Notes</h1>
    <button class="btn primary" id="saveBtn">Save</button>
    <span class="saved" id="savedMsg">Saved ✓</span>
  </div>
  <textarea id="editor" placeholder="Start typing…"></textarea>
  <script>
  (async () => {
    "use strict";
    const APP_ID     = "notes";
    const STATE_FILE = "state.json";
    const LS_KEY     = "securebin.notes.v1";
    let uninstalling = false;

    async function load() {  }
    async function save(text) {  }

    // Theme injection
    const themeEl = document.getElementById("sb-injected-theme");
    function injectTheme(vars) {
      themeEl.textContent = `:root,html,body{${
        Object.entries(vars).filter(([k])=>k.startsWith("--")).map(([k,v])=>`${k}:${v}`).join(";")
      }}`;
    }
    window.addEventListener("message", (e) => {
      if (e.data?.type === "SB_THEME" && e.data.vars) injectTheme(e.data.vars);
    });
    try {
      if (window.parent !== window)
        window.parent.postMessage({ type: "SB_REQUEST_THEME" }, "*");
    } catch (_) {}

    const editor   = document.getElementById("editor");
    const saveBtn  = document.getElementById("saveBtn");
    const savedMsg = document.getElementById("savedMsg");
    const stored = await load();
    if (stored?.text) editor.value = stored.text;

    saveBtn.addEventListener("click", async () => {
      await save(editor.value);
      savedMsg.classList.add("show");
      setTimeout(() => savedMsg.classList.remove("show"), 2000);
    });
    window.addEventListener("keydown", (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === "s") {
        e.preventDefault(); saveBtn.click();
      }
    });
  })();
  </script>
</body>
</html>
You're ready to build! This notes example hits every integration point. Copy it, rename APP_ID, replace the UI, and you have a fully working SecureBin app. The calendar app bundled with SecureBin follows the exact same patterns at larger scale.