Hull documentation
Tiny native desktop apps from your Vanilla‑JS, React, or Vue UI — a prebuilt C++ web‑view host you drive with npm scripts. No compiler, no Electron, no bundled browser engine.
Getting started
Quick start
Hull ships a small prebuilt native binary that renders your existing Vite app in the operating system's web view (WebView2 / WebKit / WebKitGTK) and exposes a JSON bridge to a C++ backend with batteries included: TLS HTTP, encrypted storage, OS keychain, SQLite, files, and printing. Your app stays plain JS/React/Vue.
In any existing Vite app, add the dev dependency:
npm i -D @mwguerra/hullAdd the Hull scripts to your package.json (or skip this and call npx hull dev / npx hull build / npx hull start directly):
{
"scripts": {
"dev": "hull dev", // Vite dev server in a native window (HMR)
"dev:browser": "hull dev --browser", // same bridge, UI in your browser
"build": "hull build", // single-file UI, packaged with the host
"start": "hull start", // run the packaged build
"installer": "hull installer" // wrap the build into .exe / .dmg / .deb
}
}npm run dev opens your app as a desktop window. Zero config — the window title and a per‑app storage namespace are derived from package.json, and installing the package also pulls the prebuilt host for your OS/CPU automatically.
Getting started
Integrate your project
The C++ backend and the JSON bridge (@mwguerra/hull/bridge) are identical across frameworks — only the UI layer and the optional state hook differ. Every Hull project has the dev dependency and npm scripts, a normal vite.config.js, an index.html entry, an optional .hullrc, and your UI code importing from the bridge.
Project layout
my-app/
├── package.json
├── vite.config.js
├── .hullrc # optional
├── index.html
└── src/
├── main.js|.jsx # Vite entry
├── App.vue|.jsx # (React/Vue) your root component
└── style.cssPick your framework
import { ping, db, nativeSetting, hasBridge } from "@mwguerra/hull/bridge";
// 1) call C++ and show the result
document.querySelector("#ping").addEventListener("click", async () => {
const res = await ping("hello"); // { ok: true, echo: "hello" }
document.querySelector("#out").textContent = JSON.stringify(res);
});
// 2) a two-way persisted setting (C++ stores it; C++ pushes changes back)
const theme = nativeSetting("theme");
theme.subscribe((v) => document.documentElement.classList.toggle("dark", v === "dark"));
theme.load(); // initial pull (no-op in a plain browser)
// 3) SQLite — works in the native host or browser dev mode
if (hasBridge()) {
db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"])
.then(() => db.query("SELECT * FROM notes ORDER BY id DESC"))
.then((notes) => console.log(notes));
}import { useEffect, useState } from "react";
import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
import { useNativeState } from "@mwguerra/hull/react";
export default function App() {
const [out, setOut] = useState(null);
const [theme, setTheme] = useNativeState("theme"); // like useState, persisted in C++
const [notes, setNotes] = useState([]);
useEffect(() => {
document.documentElement.classList.toggle("dark", theme === "dark");
}, [theme]);
useEffect(() => {
if (!hasBridge()) return;
(async () => {
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
setNotes(await db.query("SELECT * FROM notes ORDER BY id DESC"));
})();
}, []);
return (
<>
<button onClick={async () => setOut(await ping("hello"))}>Send to C++</button>
{out && <pre>{JSON.stringify(out)}</pre>}
<ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
</>
);
}<script setup>
import { ref, watch, onMounted } from "vue";
import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
import { useNativeState } from "@mwguerra/hull/vue";
const out = ref(null);
async function send() { out.value = await ping("hello"); }
const theme = useNativeState("theme"); // a ref; edits persist in C++, C++ pushes back
watch(theme, (v) => document.documentElement.classList.toggle("dark", v === "dark"),
{ immediate: true });
const notes = ref([]);
onMounted(async () => {
if (!hasBridge()) return;
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
notes.value = await db.query("SELECT * FROM notes ORDER BY id DESC");
});
</script>
<template>
<button @click="send">Send to C++</button>
<pre v-if="out">{{ out }}</pre>
<ul><li v-for="n in notes" :key="n.id">{{ n.body }}</li></ul>
</template>Run it
npm run dev # native window with HMR (+ a dev inspector tab)
npm run dev:browser # run the UI in your browser with the full bridge, no recompile
npm run build # single-file the UI + package with the host -> ./release
npm run start # run the packaged app
npm run installer # wrap the build into a native .exe / .dmg / .debGetting started
Talking to the backend
Every call goes UI → C++ and returns a Promise; all the real work happens in the native host.
import { ping, httpPost, saveCredential, isNative } from "@mwguerra/hull/bridge";
await ping("hello"); // { ok: true, echo: "hello" }
const res = await httpPost("https://api.example.com/x", { a: 1 }); // TLS, in C++
await saveCredential("api.example.com", "default", token); // OS keychainStructured persistence with embedded SQLite (parameterized, stored per‑user):
import { db } from "@mwguerra/hull/bridge";
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
await db.exec("INSERT INTO notes (body) VALUES (?)", ["hello"]);
const notes = await db.query("SELECT * FROM notes ORDER BY id DESC");Files and uploads — write bytes, read them back, build a preview URL:
import { files } from "@mwguerra/hull/bridge";
await files.write(file.name, file); // string | Uint8Array | ArrayBuffer | Blob
const bytes = await files.read(file.name); // Uint8Array
const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
imgEl.src = url; // preview; URL.revokeObjectURL(url) laterTwo-way persisted state is plaintext by default and encrypted at rest in the secure build. Use useNativeState in React/Vue, or nativeSetting directly in Vanilla JS.
Reference
Bridge API reference
All from @mwguerra/hull/bridge.
| Function | Backend |
|---|---|
ping(text) | Sync echo (diagnostics). |
httpPost / httpGet | cpp-httplib + OpenSSL, on a worker thread; injects a Bearer token from the keychain. |
saveSetting / loadSetting / loadAllSettings | Per-user store (plaintext by default; AES in the secure build). |
nativeSetting(key) | Two-way store: .get() / .set(v) / .subscribe(fn) / .load(). |
saveCredential / credentialExists / eraseCredential | OS keychain; write-only — secrets never return to JS. |
listPrinters / printMessage / printReceipt / printNetwork | Winspool / CUPS text printing, raw ESC/POS receipts, port-9100 network devices. |
db.query / get / exec / batch / migrate | Embedded SQLite, parameterized, per-user storage. |
files.write / read / readText / list / remove | File/upload storage in the per-user dir (through the secure layer). |
appInfo() | { ok, appId, secure } — secure true on a crypto build. |
bridge.on(event, fn) | Subscribe to C++ → UI push events; returns an unsubscribe fn. |
hasBridge() / isNative() / bridgeMode() | Reachability; native web view; "native" / "http" / "none". |
Framework hooks: useNativeState(key) from @mwguerra/hull/vue (returns a ref) and @mwguerra/hull/react (returns [value, setValue]).
Reference
CLI commands
| Command | What it does |
|---|---|
hull dev | Vite dev server rendered in a native window (HMR) + a dev inspector tab. |
hull dev --browser | Run the UI in your browser with the full bridge over HTTP/SSE (no recompile). |
hull build [vX.Y.Z] | Single-file the UI and package it with the host into release/<version>/<platform>/ + an archive. |
hull build … --platform <key|all> | Also package other platforms whose host binary is present; --format zip|tar.gz. |
hull start [vX.Y.Z] | Run a packaged build. |
hull installer [vX.Y.Z] | Wrap an existing build into a native installer — .exe (Inno Setup) / .dmg / .deb. Run hull build first. |
hull doctor | Check this machine for everything Hull needs — host binary, web-view runtime, system libraries — with copy-pasteable fixes. |
hull eject | Copy the C++ host project into ./desktop to add native bindings. |
Flags reach Hull through npm run only after a -- separator: npm run dev -- --browser works, but npm run build --platform all (no --) silently drops the flag. npx hull … avoids the footgun entirely. If anything misbehaves, npx hull doctor checks the machine, and --debug on dev/start prints a verbose host log and opens the web-view devtools.
Reference
Configuration (.hullrc)
Drop a .hullrc (JSON) in your project root — only the keys you set override the package defaults. Lookup order: .hullrc → .hullrc.json → hull.config.json.
{
"appId": "com.you.notes",
"secure": false,
"window": { "title": "Notes", "width": 1200, "height": 800, "icon": "build/icon.png" }
}| Key | Default | Meaning |
|---|---|---|
appId | com.hull.<pkg> | Namespaces the store, DB, files, and keychain so multiple Hull apps never collide. |
window.title | pkg name | Native window title. |
window.width / height | 1100 / 760 | Window size. |
window.icon | bundled logo | PNG/ICO for the window/app icon. SVG is not a valid native icon. |
secure | false | Run the crypto host build: AES files/settings + SQLCipher DB. |
debug | false | Open the web-view dev tools. |
description / author / license | from package.json | Installer & store metadata (publisher name, SPDX license) — set here only to override. |
linux.sandbox | auto | WebKitGTK sandbox: true force on, false force off; unset = the host probes user namespaces. |
outDir | dist | Vite UI build dir. |
releaseDir | release | Packaged-app output dir. |
Guides
Develop in the browser
Run the UI in your browser with full Vite HMR while bridge calls still reach the real native backend over HTTP/SSE — change a label, hit reload, no recompile.
npm run dev -- --browser # or: npx hull dev --browserBoth hull dev and --browser also open a dev‑only inspector (live bridge calls, events, DB/file ops, timings) that is stripped from production builds via import.meta.env.DEV dead‑code elimination.
Guides
Versioned releases
npm run build # -> release/development/<platform>/ + archive
npm run build -- v1.2.3 # -> release/v1.2.3/<platform>/ + archiveEach build emits a self‑contained, versioned bundle and a ready‑to‑ship archive (.zip on Windows, .tar.gz on macOS/Linux) with the minimal runnable set — the host binary, the libraries it needs, your inlined app.html, a double‑click launcher, and icon.png if you configured one. Unpack on the target and run.
--platform all also packages other platforms whose host binary is installed (realistically produced via CI, one runner per OS). With secure: true, bundle dirs and archives get a -secure suffix.
Guides
Native installers
After hull build, wrap the bundle for the current platform into a real installer end users expect — no extra config; the metadata comes from package.json / .hullrc (description, author → publisher, license).
npm run build # 1) package the app -> release/development/<platform>/
npm run installer # 2) wrap it in an installer -> release/development/<App>-<platform>.exe|dmg|deb
npm run build -- v1.2.3 && npx hull installer v1.2.3 # versioned| OS | Output | What it does |
|---|---|---|
| Windows | .exe | Inno Setup; per-user install (no admin), Start Menu + Desktop shortcuts, uninstaller. |
| macOS | .dmg | hdiutil; a .app bundle plus an Applications drop‑link. |
| Linux | .deb | dpkg-deb; installs to /opt, adds a .desktop entry + icon, declares library deps automatically. |
Each installer is produced on its own OS (the packaging tools are OS‑native). On Windows, install Inno Setup once: winget install JRSoftware.InnoSetup.
Guides
Security
The default build ships no crypto — everything fast, plaintext at rest, with secrets still in the keychain. For encryption at rest, use the secure build:
npm run build:host:secure # AES for files/settings + SQLCipher for the DB
# then in .hullrc: { "secure": true }Files and the DB go through one crypto layer — nothing calls cryptography directly. SQLite is hardened in all builds:
PRAGMA trusted_schema=OFFon every connection.- The default build compiles with
SQLITE_OMIT_LOAD_EXTENSIONandSQLITE_DQS=0. - Queries are always parameterized (bound in C++);
exec/query/getrun one statement each.
Guides
Custom native code
Need your own C++ binding? Run hull eject to copy the host project into ./desktop, add a binding, and build it with CMake. The standard bindings (HTTP / storage / keychain / printing / DB / files) are already there to extend.
d.on("myThing", (args, reply) => reply({ ok: true }));See desktop/README.md for the full native workflow.
Platform
Platform support
| Windows x64 | macOS (Apple Silicon) | Linux x64 | |
|---|---|---|---|
| Web view | WebView2 (Edge) | WebKit | WebKitGTK 6 |
| Credentials | Credential Manager | Keychain | libsecret |
| Printing | Winspool | CUPS | CUPS |
| Window icon | runtime (GDI+) | app bundle | .desktop entry |
| Build host on | Windows | macOS | any OS via Docker |
Prebuilt hosts ship for win32‑x64, darwin‑arm64, and linux‑x64. macOS support is Apple Silicon only — Intel Macs aren't supported. End users only need the OS web‑view runtime (preinstalled on Windows 11 and macOS; libwebkitgtk‑6.0 on Linux). A host must be built on its own OS — true cross‑compile isn't realistic for WebView2/WebKit — except Linux, which builds from any OS via Docker.
Linux dev machines need the host's runtime libraries: sudo apt install libwebkitgtk-6.0-4 libsecret-1-0 libcups2. hull dev/start check up front and print the exact command for your distro; the .deb from hull installer declares them automatically for end users.
Platform
How it works
- Prebuilt host binaries are delivered as platform‑gated optional dependencies (
@mwguerra/hull‑win32‑x64, …) — npm installs only the one for your machine. hull builduses your project's Vite plusvite‑plugin‑singlefileto inline the whole UI into one HTML file, then bundles it with the host.- The host loads that file at runtime (
--app) in production, or your dev server (--url) during development. The bridge is exposed over the web view natively, or over HTTP/SSE in browser dev mode.
License: MIT. Hull is @mwguerra/hull on npm.