What it is. A Tauri NSIS installer, unsigned (the owner’s decision — docs/70-market/PROCUREMENT_TO_SHIP.md),
that installs the GUI (VFlin Antik.exe) and, beside it, the three sidecars: the daemon vflind.exe, the CLI
vflin.exe, the MCP server vflin-mcp.exe. The core is not in the installer: the shell downloads it on first
run from the package source shipped in the bundle’s install manifest (A87: core.ensure). Every number below
was measured on this machine on 2026-09-05; the proof (§5) ran on a clean VFLIN_HOME.
1. Build it from nothing
# prerequisites: Rust (MSVC toolchain), Node 24 + pnpm, the NSIS bundler Tauri fetches on first use
cd ui && pnpm install
# the source the installed product will fetch the core from; a CDN placeholder when unset
export VFLIN_PACKAGE_SOURCE="https://cdn.example/core" # or file:///D:/vflin-core-pkg for a local proof
pnpm tauri build
tauri build runs the beforeBuildCommand — pnpm build && node scripts/build-sidecars.mjs — which builds the
web bundle, then builds the three sidecars in release from the root workspace (honouring CARGO_TARGET_DIR
when set, else the root target/), copies them to ui/src-tauri/binaries/<name>-<host triple>.exe (Tauri’s
naming for bundle.externalBin), and writes ui/src-tauri/resources/install.json ({"packageSource": …}).
Both directories are gitignored: the binaries are artifacts, the manifest can carry a machine path. Then the
app crate builds (its own standalone workspace under ui/src-tauri, its own target/), and NSIS packs
ui/src-tauri/target/release/bundle/nsis/VFlin Antik_0.1.0_x64-setup.exe.
Sizes (2026-09-05, this machine): sidecars vflind.exe 14.2 MB · vflin.exe 2.9 MB · vflin-mcp.exe 2.4 MB (19.5 MB); the installer VFlin Antik_0.1.0_x64-setup.exe 7.4 MB (the 2026-07-26 GUI-only one was 2.8 MB); tauri build rc=0 in 321 s with the web bundle, the sidecar step and the app crate in release. The core is not inside: the first run fetches the 127.7 MB package (A87) from the manifest’s source.
2. What lands where
| path | |
|---|---|
the app + sidecars (per-user install, NSIS currentUser) |
%LOCALAPPDATA%\VFlin Antik\ — VFlin Antik.exe, vflind.exe, vflin.exe, vflin-mcp.exe, resources\install.json |
| the data directory — daemon store, discovery, logs, the installed core | ~/.vflin (%USERPROFILE%\.vflin), or $VFLIN_HOME when set |
| the discovery file every client reads | ~/.vflin/local-api.json — {port, token, pid, apiVersion}, user-private |
| the daemon’s journal · the sidecar’s stdout/stderr | ~/.vflin/daemon.log · ~/.vflin/logs/vflind.out.log |
| the core, once installed | ~/.vflin/core/<vflin-core-…>/chrome.exe + core-manifest.json; ~/.vflin/core/current.json names it |
Why ~/.vflin and not %LOCALAPPDATA%\VFlin. The brief left the root to what the daemon already writes.
The daemon, the shell, the CLI (vflin-client) and the MCP server all resolve $VFLIN_HOME, else ~/.vflin;
nothing reads %LOCALAPPDATA%\VFlin, and the operator’s live store — 121 profiles — is under ~/.vflin.
Changing the default would be a migration of that store, which is a decision, not an installer default. One root,
four readers, unchanged. (docs/30-core/CORE_PACKAGE.md said %LOCALAPPDATA% in its first draft; corrected.)
3. What the first run does
- The shell starts the daemon (
daemon_ensure, a Tauri command in Rust): it asks the discovery file’s daemon forsystem/health— a stale file whose writer is gone answers nothing and does not count — and when nothing answers it spawnsvflind.exefrom beside the app, detached (own process group, no console window,VFLIN_HOMEpassed through, stdout/stderr to~/.vflin/logs/vflind.out.log), then waits up to 20 s for health before the UI reads discovery. Two shells racing to the same conclusion start at most one daemon: the daemon refuses to start on a store another daemon already serves (it asks that daemon for health with the token in the file, logs “another vflind already serves this store”, exits 0). That guard sits before the startup sweeps on purpose — those sweeps reconcile everyRUNNINGprofile toDIRTYon the assumption that no daemon runs, so a second instance would have declared the first one’s open browsers stopped. - The shell installs the core (
core_bootstrap): readsresources/install.json, callscore.ensure {source}, pollscore.statusevery 500 ms and emits a Tauri eventcore-progresswith the job (stage,bytesDone,bytesTotal,state) on every change; returns the final job. On a machine that already has the core this is a no-op in milliseconds (noop:true). The daemon persists the source that worked aspackageSourcein~/.vflin/core.jsonafter a successful install — so a latervflin core ensureneeds no argument, and the installer never writes a daemon file. The first-run screen that renders the event is design’s (G40/next); the method and the event are here. - Closing the window does not stop the daemon. The operator’s browsers live in it, and an orderly daemon
exit closes and seals every one of them (
A86). Stopping is explicit:daemon_stop_allin the shell →system.shutdownin the Local API (vflin system shutdownon the CLI,vflin_system_shutdownfor an agent) — the goodbye path, with the answer naming how many cores it closes. A detached daemon has no console to receive Ctrl-C on, which is why this is an API method and not a signal. - After a reboot the daemon is not running (no autostart is registered — see §6); the next start of the app starts it again, in seconds, and the core is already there. No reboot is required after installing.
4. What the user sees at install (unsigned)
Windows SmartScreen: “Windows protected your PC” with no publisher name. Click More info → Run anyway.
This is the known cost of shipping without a code-signing certificate (PROCUREMENT_TO_SHIP.md §3: a certificate
does not remove the warning immediately either; reputation does). Some antivirus products flag unsigned installers
on first sight; the sha256 of the installer is published beside it so a download can be verified by hand.
5. Proof — a clean store, the installer, the first run
Measured 2026-09-05 with a88_proof.py: the installer run silently (/S), the INSTALLED app launched with VFLIN_HOME pointing at an empty directory of mine (the owner’s ~/.vflin untouched), the install manifest’s source = the A87 package (file:///D:/vflin-a87-pkg-xz).
| step | result |
|---|---|
| installer /S | rc=0, 4.0 s, VFlin Antik.exe 11.0 MB, vflind.exe 14.2 MB, vflin.exe 2.9 MB, vflin-mcp.exe 2.4 MB |
| app launched → daemon up by itself | yes, 3.8 s, daemon pid 60892 |
| the daemon is the INSTALLED sidecar | %USERPROFILE%\AppData\Local\VFlin Antik\vflind.exe |
| core.ensure driven by the app from install.json | installed=True, activeSource=package, job done in 10.1 s |
| installed chrome.dll sha256 == A87’s | d2996cbb…2346 same |
| packageSource persisted by the daemon | file:///D:/vflin-a87-pkg-xz |
| first profile started (headed) from the INSTALLED core | start 200 in 6.7 s; executable under ~/core/vflin-core-… |
vflin system shutdown |
accepted; daemon gone in 24.8 s; shutdown: closed 1 of 1 core(s) gracefully, 1 sealed to LOCAL_CACHED, in 24.0s; chrome left 0 |
No reboot was needed at any point; the app started the daemon itself, the daemon installed the core itself, and the first profile ran from the installed core with the same chrome.dll hash A87 measured in three places.
6. What this does not do (on purpose, or not yet)
- No autostart. The daemon starts when the app starts. A logon-time start (Task Scheduler / Run key) would make browsers survive the app being closed AND the user logging back in — but it is a persistence entry an installer writes into the user’s session, and that is its own decision (ALPHA_PLAN row 2 lists it; not taken here).
- No signature, no updater. ALPHA_PLAN rows 10 and procurement.
local-unlock.jsonis not in the bundle — it is the developer’s local factor and stays a file the operator creates;check-no-secretsruns on the tree.- The CLI and MCP server are installed but not on
PATH:%LOCALAPPDATA%\VFlin Antik\vflin.exeis the full path; an MCP client’smcpServers.commandpoints at…\vflin-mcp.exe(MCP_SKILL_PACK.md). Adding the directory to the user’sPATHis a one-line NSIS hook, not written here.