@mwguerra/hull · v0.1.0

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:

terminal
npm i -D @mwguerra/hull

Add the Hull scripts to your package.json (or skip this and call npx hull dev / npx hull build / npx hull start directly):

package.json
{
  "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.css

Pick your framework

src/main.js
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));
}
src/App.jsx
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>
    </>
  );
}
src/App.vue
<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 / .deb

Getting 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 keychain

Structured 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) later

Two-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.

FunctionBackend
ping(text)Sync echo (diagnostics).
httpPost / httpGetcpp-httplib + OpenSSL, on a worker thread; injects a Bearer token from the keychain.
saveSetting / loadSetting / loadAllSettingsPer-user store (plaintext by default; AES in the secure build).
nativeSetting(key)Two-way store: .get() / .set(v) / .subscribe(fn) / .load().
saveCredential / credentialExists / eraseCredentialOS keychain; write-only — secrets never return to JS.
listPrinters / printMessage / printReceipt / printNetworkWinspool / CUPS text printing, raw ESC/POS receipts, port-9100 network devices.
db.query / get / exec / batch / migrateEmbedded SQLite, parameterized, per-user storage.
files.write / read / readText / list / removeFile/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

CommandWhat it does
hull devVite dev server rendered in a native window (HMR) + a dev inspector tab.
hull dev --browserRun 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 doctorCheck this machine for everything Hull needs — host binary, web-view runtime, system libraries — with copy-pasteable fixes.
hull ejectCopy 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.jsonhull.config.json.

.hullrc
{
  "appId": "com.you.notes",
  "secure": false,
  "window": { "title": "Notes", "width": 1200, "height": 800, "icon": "build/icon.png" }
}
KeyDefaultMeaning
appIdcom.hull.<pkg>Namespaces the store, DB, files, and keychain so multiple Hull apps never collide.
window.titlepkg nameNative window title.
window.width / height1100 / 760Window size.
window.iconbundled logoPNG/ICO for the window/app icon. SVG is not a valid native icon.
securefalseRun the crypto host build: AES files/settings + SQLCipher DB.
debugfalseOpen the web-view dev tools.
description / author / licensefrom package.jsonInstaller & store metadata (publisher name, SPDX license) — set here only to override.
linux.sandboxautoWebKitGTK sandbox: true force on, false force off; unset = the host probes user namespaces.
outDirdistVite UI build dir.
releaseDirreleasePackaged-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 --browser

Both 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>/ + archive

Each 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
OSOutputWhat it does
Windows.exeInno Setup; per-user install (no admin), Start Menu + Desktop shortcuts, uninstaller.
macOS.dmghdiutil; a .app bundle plus an Applications drop‑link.
Linux.debdpkg-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=OFF on every connection.
  • The default build compiles with SQLITE_OMIT_LOAD_EXTENSION and SQLITE_DQS=0.
  • Queries are always parameterized (bound in C++); exec / query / get run 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 x64macOS (Apple Silicon)Linux x64
Web viewWebView2 (Edge)WebKitWebKitGTK 6
CredentialsCredential ManagerKeychainlibsecret
PrintingWinspoolCUPSCUPS
Window iconruntime (GDI+)app bundle.desktop entry
Build host onWindowsmacOSany 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 build uses your project's Vite plus vite‑plugin‑singlefile to 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.