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.




Hardware Layer
↓
SecureBin USB Vault
↓
SecureOS Runtime
↓
Sandbox Execution Engine
↓
SecureBin SDK
↓
Application
Security model
| Area | Goal | Why it matters |
|---|---|---|
| Vault encryption | Protect data at rest | Keeps sensitive files and app state private inside the encrypted environment |
| Runtime isolation | Separate apps from the host | Reduces cross-app interference and improves platform trust |
| Permission model | Scope app capabilities | Lets users understand what an app can access before installing it |
| Offline-first execution | Work without cloud dependence | Aligns with SecureBin's sovereignty and privacy philosophy |
| No telemetry by default | Respect user ownership | Reinforces the brand promise of private computing |
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.
{
"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.
- Discover SecureBin
Learn the philosophy, architecture, and app model through the docs.
- Install the SDK
Start with templates, manifest examples, and vault-safe APIs.
- Build your first app
Create a working SecureBin-compatible application and test it inside the vault environment.
- Package and publish
Prepare the app for the store with iconography, metadata, and permission declarations.
- Monetize and iterate
Ship paid tools, privacy utilities, offline AI features, or Bitcoin-native software.



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.

.html file — HTML, CSS, and JavaScript together. No bundler, no framework, no build step required.
appData is stored encrypted on disk inside the vault, and automatically backed up to localStorage as a fallback.
postMessage. Your app stays visually in sync with the user's chosen theme.
Quick Start
The fastest path to a working SecureBin app — copy this boilerplate, customize it, and drop it into the apps folder.
- Create your HTML file
Name it something short with no spaces:
my-app.html. This becomes your app's ID. - 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
internetpermission 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 declaresinternetpermission and you want to load fonts when online, you may add a Google Fonts link as a progressive enhancement, but never rely on it. - 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. - Add the theme injection script
Copy the
injectThemeVarsandpostMessagelistener pattern. It takes about 20 lines and handles all theme sync automatically. - Initialise your storage
Call
window.usb.appData.readJSON(APP_ID, 'state.json')on startup. Fall back tolocalStorageif the bridge isn't available. - Install it
Copy your
.htmlfile into the apps folder or use the in-app installer. SecureBin will detect it immediately.
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.
<!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.
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 / Folder | Required | Purpose |
|---|---|---|
my-app.html | ✅ Yes | The app entry point — HTML, CSS, and JS. Can reference dependency files via relative paths. |
my-app.png | Optional | App icon shown in the launcher. 512×512px PNG recommended. |
my-app.meta.json | Auto | Generated by the installer. Contains display name, version, and the dependencies array. |
my-app/ | Auto | Dependency 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.
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
| Permission | What it allows | Prompt shown to user |
|---|---|---|
internet | The app may make outbound network requests (fetch, WebSocket, etc.) | "Connect to Internet" — if denied, the app is blocked from opening |
usb_upload | The app may read files from the USB vault into memory | "Access files from USB" |
usb_download | The app may write/save files to the USB vault | "Save files to USB" |
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.
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.
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 worksWhen 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().
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.
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:
- Open the Apps panel and click Import App
- Select your
.htmlfile — the importer reads theapp-dependenciesmeta tag - Optionally select an icon image
- 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
| Rule | Details |
|---|---|
URLs must be https:// | Plain http:// and relative URLs are rejected by the installer |
dest is a filename only | No slashes, no path traversal. chart.min.js ✅ — ../chart.js ❌ |
| Dependencies are unencrypted | They live in apps/<slug>/, outside the vault. Never put sensitive data in a dep file. |
| No vault unlock required | Apps and their dependencies load without requiring the vault to be unlocked. Only appData (user state) is encrypted and vault-gated. |
| Self-destruct safe | The vault self-destruct wipes .usb-vault/ only. App files and bundles in apps/ are not touched. |
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.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 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.
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.
| Variable | Light value | Dark value | Usage |
|---|---|---|---|
--bg | #f9fafb | #000000 | Page background |
--panel | #ffffff | #080808 | Card / panel backgrounds |
--panel-2 | #f9fafb | #0d0d0d | Nested / secondary panels |
--text | #111827 | #ffffff | Primary text colour |
--muted | #6b7280 | rgba(255,255,255,.50) | Secondary / helper text |
--border | #e5e7eb | rgba(255,255,255,.10) | Borders, dividers |
--border-2 | #e5e7eb | rgba(255,255,255,.06) | Subtler borders, table rows |
--accent | #2563eb | #60a5fa | Buttons, links, highlights |
--btn-bg | #ffffff | #080808 | Default button background |
--btn-text | #111827 | #ffffff | Button label text |
--btn-border | #e5e7eb | rgba(255,255,255,.16) | Button border colour |
--shadow | 0 1px 6px… | 0 4px 24px… | Box shadow values |
--progress | #3b82f6 | #22c55e | Progress bars |
--success | #16a34a | #22c55e | Success states, confirmations |
--danger | #dc2626 | #ef4444 | Errors, destructive actions |
--warn | #d97706 | #f59e0b | Warnings, 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 type | Direction | Payload | When to use |
|---|---|---|---|
SB_THEME | SecureBin → App | { type, vars } | Respond to live theme changes |
SB_REQUEST_THEME | App → SecureBin | { type } | Ask for current theme on load |
SB_APP_UNINSTALLED | SecureBin → App | { type, appId } | Clean up localStorage on uninstall |
vault:changePinProgress | System 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. |
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.
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.
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.
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 }]
Opens another installed app by its ID in a new modal window. Useful for building launcher-style apps.
await window.usb.appsOpen("calculator");
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"
);
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"
});
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 });
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);
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);
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"
});
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
});
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.
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
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"] });
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");
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");
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");
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 stylesCopy 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 iconPlace 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
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.
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.
<!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>
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.
