Offline-first inventory for multi-branch businesses.
Products, stock, orders, purchasing, and accounts — in one self-hosted binary
that keeps every branch in sync, even when branches go offline.
Vulos — rooted in vula, the Zulu and Xhosa word for open.
FlowStock is a free, open-source inventory and stock-control app for small multi-branch businesses — shops, hardware stores, wholesalers, workshops. It is a single Go binary that serves a web UI and stores everything in a local SQLite database: no cloud account, no subscription, no external services. Each branch runs its own copy, works fully offline, and syncs peer-to-peer with the other branches over the LAN, a VPN, or a tunnel whenever they can reach each other. There is no central server. Stock is an append-only ledger, so branches that traded while disconnected always converge to the same totals.
- 📦 Products & variations — categories, SKUs, price + cost price, attributes, reorder points
- 🏪 Multi-branch stock ledger — stock on hand per branch, derived from immutable movements (never a mutable counter)
- 🔄 Leaderless offline-first sync — hybrid-logical-clock oplog; catalog merges last-writer-wins, stock movements + goods receipts merge by union; any topology (pair, hub-and-spoke, mesh); authenticated, fail-closed
- 🧩 Optionally merges with the shared DMTAP Sync engine — an opt-in build
(
-tags dmtap, not what a release ships) swaps FlowStock's own CRDT for the Vulos suite's one specified, vector-verified implementation ofsubstrate/SYNC.md— the same compiled engine Diwan runs, not a second implementation that agrees most of the time. Every op is then individually signed, and two branches can prove they agree by comparing a 33-byte state root rather than two screens. It costs 2.6 MiB of binary, so it is worth building only when you need byte-identical merge semantics with a peer running the same engine — see Choosing an engine. Nodes advertise their merge engine in the handshake and refuse to sync across a mismatch, because two algebras in one mesh converge only by luck - 🏷️ Self-describing workspaces — every row and op carries a workspace
org_id; cross-workspace ops are rejected, and a new device pairs in by adopting the workspace rather than starting its own - 📁 Folder sync (no network needed) — replicate through Dropbox, Google Drive, Syncthing, a NAS, or a USB stick (sneakernet); each device writes only its own append-only file, so file-sync never conflicts
- 🗜️ Bounded history — one-click compaction writes a checksummed, signed snapshot and prunes ops every branch has acknowledged
- 🔑 Mutual key-authenticated sync — each node has an Ed25519 identity; op batches and snapshots are signed, and every sync request is signed and verified against the peer's enrolled key (±5-min freshness, replay-protected). The shared secret only bootstraps pairing; revoke a node by removing its peer row
- 🧾 Sales orders — draft → confirm (deducts stock) → paid; cancelling reverses stock; product + service line items
- 🚚 Purchasing — purchase orders with VAT, goods receiving with partial receipts (an immutable per-receipt ledger) and automatic status progression
↔️ Adjustments, counts & transfers — audited movements between branches- 💰 Creditors & debtors — balances computed from orders, POs and recorded payments
- 📊 Real dashboard & reports — sales, valuation, movements, low stock, accounts; CSV export everywhere
- 🧪 Demo mode — the UI runs in a plain browser with seeded data
(
npm run dev), so you can try everything with zero setup - 🌓 Light & dark themes; single ~12 MB release binary; runs on a laptop, server, NAS or Raspberry Pi
Docker:
docker run -p 8787:8787 -v flowstock-data:/data \
-e FLOWSTOCK_HOST=0.0.0.0 -e FLOWSTOCK_DATA_DIR=/data \
ghcr.io/vul-os/flowstock:latestBinary — install.sh downloads the archive for your platform and verifies
it against the release's own SHA256SUMS.txt before unpacking it:
curl -fsSLO https://raw.githubusercontent.com/vul-os/flowstock/main/install.sh
less install.sh # review before running
sh install.sh
./flowstock # serves http://127.0.0.1:8787Downloading an asset by hand from Releases?
scripts/verify.sh is the same contract with per-failure
exit codes, and needs only curl and sha256sum/shasum:
curl -fsSLO https://raw.githubusercontent.com/vul-os/flowstock/main/scripts/verify.sh
bash verify.sh --tag v0.3.0 --attest flowstock_v0.3.0_linux_amd64.tar.gz
bash verify.sh --dir ~/Downloads flowstock_v0.3.0_linux_amd64.tar.gzBoth it and install.sh fail closed: a missing, empty, HTML or malformed
SHA256SUMS.txt, a manifest with no entry for the asset you asked for, a
truncated download, or a digest mismatch each abort with their own diagnostic,
and nothing is installed. There is deliberately no --skip-verify and no path
where an absent manifest means "nothing to check" — a verifier that shrugs at a
404 prints a line that looks like verification while checking nothing, which is
worse than no verifier because it turns "I don't know" into "it's fine".
Releases also carry a sigstore build-provenance attestation, signed with a
short-lived certificate minted from the release workflow's OIDC identity — no
long-lived key, no secret, nothing to rotate. --attest (and install.sh,
when the gh CLI is present) checks it; without gh both say provenance was
not checked rather than letting a pass imply more than it checked.
make release-guards runs both failure matrices — 24 synthetic-origin cases
for the verifier, 14 for the installer — so the refusals are exercised rather
than assumed.
From source (Go 1.25+, Node 18+):
git clone https://github.com/vul-os/flowstock.git
cd flowstock
npm install
npm run build:all # builds the single ./flowstock binary (frontend embedded)
./flowstockZero-setup demo (browser only, seeded data):
npm install && npm run dev # → http://localhost:5173Connect a second branch: run each with FLOWSTOCK_HOST=0.0.0.0, set the
same sync secret on both (Settings → Sync) to pair them — after pairing they
authenticate each other by key — add the other's URL (http://<host>:8787) as a
peer, Sync now. Details in
docs/GETTING-STARTED.md.
flowchart LR
subgraph JHB["Branch A — Johannesburg"]
UA[Browser UI] --> CA[Go binary]
CA --> DA[(SQLite + oplog)]
end
subgraph CPT["Branch B — Cape Town"]
UB[Browser UI] --> CB[Go binary]
CB --> DB[(SQLite + oplog)]
end
CA <-- "HTTP /api/sync (mutual Ed25519 key auth)<br>LAN · VPN · your own cloud node" --> CB
Every mutation is journalled to an oplog with a hybrid-logical-clock timestamp
and tagged with the workspace org_id. Sync exchanges ops (push + pull,
batched, idempotent, signed): catalog rows resolve last-writer-wins; stock
movements and goods receipts are immutable and merge by union, which is what
makes offline multi-branch stock safe. Version vectors are derived from the
oplog, so sync is stateless and any node can relay any other node's changes.
The same ops can travel over HTTP or a shared folder (Dropbox/Syncthing/USB),
and one-click compaction snapshots state and prunes acknowledged history.
Which write wins is decided by FlowStock's own CRDT in the binary a release
ships. A -tags dmtap build hands that job to the shared DMTAP Sync engine
instead: storage, transport and identity are unchanged — SQLite, the
mutual-Ed25519 HTTP pull and the folder path, the per-node key — and the
substrate supplies only the algebra, catalog rows mapping to its
last-writer-wins register and the two ledgers to its add-only set, which is
exactly the behaviour described above. Because the two engines break an exact
timestamp tie differently, the merge engine is part of the sync handshake and a
round across a mismatch is refused rather than allowed to diverge in silence.
When the extra 2.6 MiB is worth it is spelled out in
Choosing an engine; full details in
docs/ARCHITECTURE.md and docs/SYNC.md.
Zero-config by default. Optional flowstock.config.json or env vars set the
port, bind host, data directory, an owner password, and frame_ancestors (for
Vulos OS embedding). Business, branches and sync are configured in-app. See
docs/CONFIGURATION.md.
| Doc | Contents |
|---|---|
| docs/OVERVIEW.md | what FlowStock is, in one page — also the docs site landing page |
| docs/GETTING-STARTED.md | install, first run, connecting branches, backups |
| docs/ARCHITECTURE.md | Go binary, data model, oplog & clocks, sync protocol |
| docs/SYNC.md | topologies, security, merge semantics, conflict examples |
| docs/CONFIGURATION.md | every setting + environment variables |
| docs/CLOUD-NODE.md | a branch node on a VPS: which build, TLS, firewall, backup, upgrade |
| docs/SOVEREIGNTY.md | how FlowStock meets the substrate's five adoption properties — and where it does not |
| docs/SCREENSHOTS.md | regenerating the README screenshots |
| docs/TESTING.md | Go and browser test suites, running and debugging them |
npm run dev # UI only, browser + demo data (port 5173)
npm run dev:app # Go server proxying to the Vite dev server
npm run build # frontend production build
npm run build:all # single embedded binary
npm run lint # eslint
npm run test:go # Go unit + HTTP sync e2e tests
npm run test:e2e # browser end-to-end tests (Playwright)
npm test # both suites
npm run screenshots # regenerate docs/screenshots (Playwright)The Go test suite includes real two-node tests covering the
offline-divergence → reconvergence path over HTTP and over a shared folder
(backend/internal/sync/), workspace isolation + pairing, concurrent
goods-receipt convergence, oplog compaction with snapshot rebuild, and the
signed-batch tamper check, plus the store's merge/ledger invariants
(backend/internal/store/).
npm run test:e2e drives the real binary in a real browser — never the
demo data. It builds the single embedded binary (skipped when already current),
boots each instance against a throwaway data dir on a free port, and runs the
flows a shopkeeper actually walks: adding a product and variant, recording
stock, confirming an order, receiving a purchase order, and moving stock
between branches.
The centrepiece is a two-node convergence test: two separate processes with
separate databases are paired through the Setup screen's "Join a branch" tab,
edited while apart — including concurrent stock movements at the same branch —
then synced, with convergence asserted in both browsers. A folder-sync test
proves the same thing with the network peers deleted, so only the shared
ops-<node>.jsonl files can carry the data.
One-time browser install:
npx playwright install chromiumSee docs/TESTING.md for the layout, how to debug a failure, and the conventions that keep the suite fast and non-flaky.
Issues and PRs are welcome. Keep changes small and focused; run npm test
(Go + browser) and npm run lint before submitting. For anything protocol-level
(oplog, merge rules, sync endpoints), open an issue first — on-disk and on-wire
compatibility matters.
The mark in brand/ is the source of truth. Every icon this repo
ships — favicon, PWA and app icons, the mark in the README and on the site — is
rendered from brand/logo.svg rather than redrawn, so there is one approved
drawing and no second copy to drift.
Copy it outward, never edit a derived copy, and never edit brand/ to match
something downstream.
MIT OR Apache-2.0 — © VulOS. FlowStock is a VulOS project; source and issues at github.com/vul-os/flowstock.
![]()
vulos — open by design








