Development Setup
Local development setup for Vaulthalla contributors.
Development Setup
This guide is for contributors getting from clone to useful local work.
If you are doing docs-only work, you do not need a runtime environment. If you are touching the core daemon, you need Linux or a Mac; if you are touching FUSE, Debian packaging, systemd, TPM/swtpm or the Linux install lifecycle, you need Linux.
Platform Assumptions
The core builds and its unit tests run natively on Linux (GCC, libstdc++) and on Apple Silicon Macs (Apple clang, libc++). OS-specific code lives behind small seams in core/include/platform with linux and darwin implementations chosen by Meson, so one code base serves both.
| Area | Where it can be validated |
|---|---|
| Core daemon logic, storage, sync, auth, RBAC, protocols | Linux or macOS (meson test) |
| FUSE behavior | Linux (the macOS package has no FUSE bridge) |
| Debian packaging and maintainer scripts | Linux |
systemd units, TPM/swtpm, make test / make itest harness | Linux |
launchd units, vaulthalla-provision, vaulthalla-caddy, the macOS package | macOS (contract tests also run on Linux CI) |
| Web console | Either |
Debian and Ubuntu-style environments remain the reference: the integration harness, the Debian package and the release pipeline assume apt, sudo and systemd. A green macOS build does not validate Linux-only behavior, and the reverse is also true.
Quick Health Check
From the repo root:
1bash tools/dev/verify.sh doctor
The doctor profile checks for git, python3, node, pnpm, meson, and ninja, flags Node and pnpm versions that drift from web/.nvmrc and the packageManager pin, and reports a missing web/node_modules, missing private web icons, an unconfigured build/ directory, and release-version drift.
Toolchains
Streamlined local install via Makefile
If you want the repo's higher-level entrypoints instead of driving the scripts manually, the top-level Makefile is the streamlined route.
Common targets:
1make install2make dev3make test4make itest5make clean-test6make clean
What they do at a high level:
make installruns the normal install guard, then./bin/install.sh(the production install; Linux only, as before)make devruns./bin/uninstall.sh, then the install guard, then./bin/install.sh, all for the isolated dev instance (see below);make dev-planprints what it would do,make dev-uninstallremoves the dev instancemake testruns the DB-backed unit tests against a database created for that run, then drops itmake itestruns the integration proofs as a private daemon instance (own database, paths and FUSE mount)make clean-testremoves leftovers of interrupted test runs, nothing else
make install and make dev are host-mutating install workflows. The test targets are not: they never install
anything, never change the development install, and never touch a database they didn't create. They need a one-time
sudo tools/harness/bootstrap.sh and passwordless sudo.
Web/admin work
The web client uses:
- Node pinned by
web/.nvmrc(24.13) pnpm- Next.js, React, TypeScript, ESLint
Setup:
1cd web2pnpm install --frozen-lockfile3pnpm test4pnpm dev
Release work
Releases use vl-release (vlr), installed from the ValkyrianLabs APT
repository (sudo apt install vl-release). release.toml configures it; the repository needs no Python packages.
1vlr doctor2vlr check3vlr status
Core/runtime work
For Linux contributors on a machine or VM you are comfortable modifying, the repo provides an install-deps helper:
1./bin/setup/install_deps.sh
That script configures the Vaulthalla APT repository and installs native dependencies such as Meson, Ninja, PostgreSQL, FUSE, libsodium, libtss2, swtpm, and related packages. The two Vaulthalla-built SDKs come only from apt.vaulthalla.sh: libpdfium-dev (PDFium) and libpqxx-vh-dev (a static libpqxx 8 build; the package build links it in, so servers need only libpq5). Ubuntu's libpqxx-dev is not used.
Run it deliberately. It changes the host.
Preferred Dev Workflow
The fastest rebuild-and-reinstall loop in this repo is the dev workflow behind make dev.
It installs an isolated dev instance (meson -Dinstance=dev, bin/lib/platform.sh) that can sit beside a
production install on the same host and never touches it:
| Production | Dev instance | |
|---|---|---|
| Linux prefix | /usr | /opt/vaulthalla-dev |
| Linux paths | /etc, /var/lib, /var/log, /run …/vaulthalla, /mnt/vaulthalla | …/vaulthalla-dev, /mnt/vh_dev |
| Linux units | vaulthalla.service, -web, -swtpm | vaulthalla-dev.service, -web, -swtpm (swtpm on 2421) |
| macOS prefix | the package (/usr/local) | /usr/local/vaulthalla-dev |
| macOS paths | /Library/Application Support/Vaulthalla, /Library/Logs/Vaulthalla | …/Vaulthalla-dev |
| macOS daemons | sh.vaulthalla.{server,web} | sh.vaulthalla.dev.{server,web,postgres} |
| Account | vaulthalla (macOS _vaulthalla) | vaulthalla-dev (macOS _vaulthalla_dev) |
| Database | vaulthalla | vaulthalla_dev (macOS: its own cluster on 127.0.0.1:5532) |
| Ports | web 36968, ws 36969, http 36970, s3 39000 | +100 |
| CLI | vh | vh-dev |
It is fast because it starts from a fresh dev instance every time: make dev begins by running ./bin/uninstall.sh,
which wipes the dev instance's directories, database and account (and only those) with no interactive warning once
dev mode is active. The vaulthalla.dev nginx endpoint (bin/setup/install_dev_nginx.sh) belongs to production and is
skipped for the instance. VH_INSTANCE= make dev restores the old behavior (a dev build installed over production's
paths) on a box where that is what you want.
On macOS, make dev is the only source install: production comes from the installer package or the Homebrew cask.
It needs Homebrew (tools/macos/bootstrap.sh runs as you), node and pnpm, and sudo for the privileged steps;
make dev-plan (VH_DRY_RUN=1) prints every privileged command without running any of them.
Dev mode guardrails
The repo requires two guardrails before the destructive dev workflow is considered valid:
- set
VH_BUILD_MODE=dev - create a repo-root sentinel file named
enable_dev_modewhose contents are exactlytrue
Example:
1export VH_BUILD_MODE=dev2printf 'true\n' > enable_dev_mode3make dev
This behavior is enforced by bin/lib/dev_mode.sh. If VH_BUILD_MODE=dev is set without the sentinel, the scripts fail. If the sentinel exists but VH_BUILD_MODE is not dev, the scripts warn about the mismatch.
If you use this workflow, do it on a machine or VM where destroying the local Vaulthalla install is acceptable.
Custom install path
If you want the same install plumbing without using the Makefile, the equivalent lower-level path is:
1./bin/install.sh
For destructive dev-mode reinstall behavior, the Makefile target is just wrapping:
1./bin/uninstall.sh2./bin/install.sh
Core Build Path
The current CI-style core build path is Meson-based:
1meson setup build --buildtype=debug -Dwerror=true -Dbuild_unit_tests=true -Dinstall_data=false 2>&1 | tee setup.log2bash tools/dev/quiet-build.sh build setup.log3meson test -C buildThat matches the shape used in .github/actions/build/action.yml and .github/actions/test/action.yml.
1tools/macos/bootstrap.sh # once: Brewfile, pinned PDFium, pkg-config aliases (runs as you)2source tools/macos/env.sh # every shell: pkg-config paths, openssl@3 first, system include paths3meson setup build-macos -Dwerror=true -Dbuild_unit_tests=true 2>&1 | tee setup.log4bash tools/dev/quiet-build.sh build-macos setup.log5eval "$(tools/macos/test-db.sh up)" # a disposable PostgreSQL 16 cluster on :543296meson test -C build-macos7tools/macos/test-db.sh downenv.sh pins OpenSSL 3 ahead of Homebrew's unversioned openssl (which may be 4.x), treats a CPATH that points at Homebrew's headers as system include paths so their warnings stay out of the build, and drops -L/-I entries for directories that no longer exist. CI runs the same steps in .github/workflows/macos.yml.
Builds Must Be Silent
Code pushed to GitHub, to a branch or a pull request, must build on Linux and macOS with no output but ninja's [n/N] progress lines: no compiler or linker warnings, no notes, no Meson WARNING or DEPRECATION lines. tools/dev/quiet-build.sh runs ninja and fails on anything else; CI runs it on both platforms with -Werror. Fix the cause of a warning; never silence it with a pragma, a weaker warning set, or by dropping -Werror. Third-party headers are the only exception and are already treated as system headers. The rule is stated in AGENTS.md at the repository root.
The repo root is the Meson project root. core/ remains the C++ subsystem, and compile_commands.json is emitted at build/compile_commands.json.
If your work needs an installed runtime rather than just a compiled binary, the repository also provides higher-level install and test helpers under bin/.
Integration And Runtime Test Path
For Linux-only runtime validation, once per machine:
1sudo tools/harness/bootstrap.sh
Then, as often as you like:
1make itest # integration proofs as a private instance2make test # DB-backed unit tests against a per-run database
What the setup does once: a Python 3.14 venv for tools/harness, a dedicated PostgreSQL role (vh_harness) whose
connection settings live in the root-only /var/lib/vaulthalla-harness/db.env, and a few nologin accounts used as
test actors. What every run does: build what it needs in build-itest/, create its own database (the integration
proofs clone a pre-migrated template), run with private /tmp/vhh_* paths, and drop everything afterwards.
This path depends on sudo, Linux, FUSE and PostgreSQL. Use it when your change affects runtime behavior rather than
simple docs or web copy. bash bin/tests/prove_isolation.sh checks that a run leaves the development install, its
binaries, environment files and database untouched.
Test database requirements
There is nothing to configure. Each run creates its own database owned by vh_harness and passes the connection
settings only to the test process. Test code refuses to run without them, and refuses to wipe any database that
isn't a test database.
Local Config And Environment Notes
- Default runtime config ships from
deploy/config/config.yaml. - Install scripts copy config into
/etc/vaulthalla/config.yamlon Linux; on macOS,vaulthalla-provisionwrites/Library/Application Support/Vaulthalla/config.yaml. - The web service unit uses
deploy/systemd/vaulthalla-web.service.in. - Some local scripts look for
deploy/vaulthalla.env.
The repo only ships deploy/vaulthalla.env.example as a public template. If you need real storage-backed test credentials or environment values, create your own local deploy/vaulthalla.env from that template and keep secrets out of commits.
R2/S3 test environment
If you want the dev workflow to carry R2 test credentials into the installed environment:
- copy
deploy/vaulthalla.env.exampletodeploy/vaulthalla.env - uncomment and fill in the
VAULTHALLA_TEST_R2_*variables
The example variables are:
1# VAULTHALLA_TEST_R2_ACCESS_KEY=<your_access_key>2# VAULTHALLA_TEST_R2_SECRET_ACCESS_KEY=<your_secret_key>3# VAULTHALLA_TEST_R2_ENDPOINT=https://<your_unique_id>.r2.cloudflarestorage.com4# VAULTHALLA_TEST_R2_REGION=auto5# VAULTHALLA_TEST_R2_BUCKET=<your_bucket_name>
When deploy/vaulthalla.env exists, bin/setup/install_dirs.sh installs it as /etc/vaulthalla/vaulthalla.env.
Optional dev R2 test vault seeding
deploy/config/config.yaml includes:
1dev:2 enabled: false3 init_r2_test_vault: false
If dev mode is enabled in config and init_r2_test_vault is set to true, the storage manager can seed an R2 Test Vault automatically during startup when the required VAULTHALLA_TEST_R2_* environment variables are present.
Keep this workflow-specific and local. Do not commit real credentials.
Known Practical Snags
Missing web/node_modules
bash tools/dev/verify.sh doctor will flag this. Run:
1cd web2pnpm install --frozen-lockfile
Web build failures caused by missing private icons
The CI web build syncs private icons from ~/vaulthalla-web-icons before pnpm build. If local web build work depends on those assets, coordinate with the maintainer instead of committing fake replacements.
Integration test scripts expecting deploy/vaulthalla.env
Some scripts reference deploy/vaulthalla.env, while the repo only includes deploy/vaulthalla.env.example. If your test path needs environment values, create the local file explicitly.
make dev is not a safe default
It is a rebuild convenience for contributors who intentionally want a fresh dev instance every time. It is not a normal "development server" command, and the dev instance's data does not survive it. A production install beside it is never touched.
Cross-platform false confidence
If you changed FUSE, Debian packaging, systemd, the Linux PostgreSQL lifecycle, or TPM behavior from a Mac, your local check is not enough; if you changed launchd, provisioning, Caddy or the macOS package from Linux, the contract tests are not a real install. Say that plainly in the PR and ask for the right validation. The macOS install is proven with tools/macos/candidate.sh stage && tools/macos/candidate.sh pkg, then sudo tools/macos/candidate.sh accept, which installs the package on the Mac and runs the full acceptance.
Where To Go Next
- Read Contribution Boundaries before picking sharp work.
- Use the Architecture Map to find the right subsystem.
- Follow the Validation Guide before opening a PR.
