Running Lattice as a Daemon
Lattice now has an explicit daemon entrypoint:
lattice daemon runlattice sync still runs the same foreground daemon for compatibility.
systemd service
Section titled “systemd service”Under systemd, run Lattice in the foreground and let systemd supervise the process. Do not use --daemonize from a unit file.
System unit:
sudo install -Dm0644 packaging/systemd/lattice.sysusers.conf /usr/lib/sysusers.d/lattice.confsudo systemd-sysusers /usr/lib/sysusers.d/lattice.confsudo install -Dm0644 packaging/systemd/lattice.tmpfiles.conf /usr/lib/tmpfiles.d/lattice.confsudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/lattice.confsudo install -Dm0644 packaging/systemd/lattice.service /etc/systemd/system/lattice.servicesudo systemctl daemon-reloadsudo systemctl enable --now lattice.servicejournalctl -u lattice.service -fUser unit:
install -Dm0755 target/release/lattice ~/.local/bin/latticeinstall -Dm0644 packaging/systemd/user/lattice.service ~/.config/systemd/user/lattice.servicesystemctl --user daemon-reloadsystemctl --user enable --now lattice.servicejournalctl --user -u lattice.service -fFor login-independent user services, enable lingering separately:
loginctl enable-linger "$USER"The unit files use Type=notify, NotifyAccess=main, and WatchdogSec=60s. Lattice sends readiness after the daemon has bound networking, created IPC, and written initial status, then sends watchdog pings while it waits for shutdown.
The unit files use systemd directory directives. At runtime Lattice honors CONFIGURATION_DIRECTORY, STATE_DIRECTORY, and RUNTIME_DIRECTORY, so the packaged service stores config.toml, daemon state, logs, PID files, status IPC, and vault data under the directories systemd creates for the service. The system unit runs as the dedicated lattice user declared by packaging/systemd/lattice.sysusers.conf; the user unit inherits the invoking user.
The system service is one explicit Lattice instance. Its paths are
/etc/lattice/config.toml, /var/lib/lattice, and /run/lattice. The packaged
tmpfiles rule makes /etc/lattice private and owned by the service account so
stopped-daemon CLI mutations can atomically replace config.toml. Administer
that instance as the service account with the global --system flag:
sudo -u lattice lattice --system vault listsudo -u lattice lattice --system statussudo -u lattice lattice --system vault credential unlockThe user unit launches ~/.local/bin/lattice and requires no system-wide binary
installation. It deliberately does not set StateDirectory: both the ordinary
CLI and service use the same platform data directory under
~/.local/share/lattice on Linux. Its configuration and runtime directories
remain the normal ~/.config/lattice and $XDG_RUNTIME_DIR/lattice paths.
Live administration
Section titled “Live administration”While the daemon is running, lattice vault add|remove, lattice vault member remove, lattice vault invite, lattice vault admin, lattice vault recover request, vault recover import, lattice vault conflicts, lattice vault secrets, credential-cache changes, and mailbox/object-storage configuration are
sent over the UID-scoped
local IPC endpoint. The daemon serializes them with vault materialization,
pauses offline-service work while changing endpoints or
capabilities, and persists config before publishing the new runtime generation.
Member removal additionally fences publication until the rotated membership
and signed cutover are durable. A removed vault’s managed KDBX data is retained.
After editing config.toml directly, apply the validated network, rendezvous,
relay, mailbox, storage, and vault configuration with:
lattice daemon reloadInvite creation and approval run through the daemon. Approval persists an
idempotent encrypted acceptance receipt before committing membership. The CLI
writes that receipt to --out; retry the same command if the output write or IPC
response fails. Join acceptance also runs through the daemon. It commits
membership and a secret-free completion receipt before deleting the pending
join, so retrying the same acceptance package resumes safely after an
interrupted transaction. The vault credential must be unlocked before
acceptance so the daemon can verify the configured vault identity before commit
and activate it immediately. Fresh-key recovery request creation works while
the daemon is running and persists the new private identity before replying, so
an exact retry emits the same request. Recovery export and fresh-key
approval/acceptance also run through authenticated IPC. The daemon quiesces and,
for membership replacement, restarts only the affected vault task so in-memory
authority cannot lag the committed membership. Package bodies never cross the
1 MiB IPC frame: the CLI passes canonical paths, and those paths must be
readable or writable by the daemon’s service user. Approval stores a private
completion receipt before pending-state cleanup, allowing an exact package
retry after a crash or lost IPC response. Peer-identity rotation is a live
daemon transaction that quiesces vault writers, drains the old transport and
authenticated sessions, installs the new identity, and resumes sync. Trusted
same-key recovery import is live: only an inactive recovery-required vault
may be replaced, and the daemon activates it after the transaction commits.
Pairing files are capped at 768 KiB; recovery packages may be much larger and
therefore remain path-based for both stopped and live operation.
Admin succession uses four authenticated operations: admin transfer creates
an old-admin-signed offer, admin accept adds the target writer’s signed
successor document, admin approve encrypts rotated epoch and bridge material
to that target and signs the exact authoritative head, and admin commit
journals and publishes the new-admin-signed cutover. Both confirmation codes
must be compared out of band. Approval is persisted before its package is
returned, commit has a durable receipt, and exact retries are idempotent. If the
head changed after approval, commit rejects the stale package and the old admin
must approve the acceptance again.
Secondary Admin recovery is opt-in. With the daemon running, the primary Admin
uses lattice vault admin recovery set <vault> <recovery-writer-device-id> or
recovery clear; stopped-daemon set/clear is refused because policy changes
must commit a journaled membership cutover and fence active sessions. Enrollment
requires three distinct devices: the primary Admin, the designated recovery
Writer, and another Writer eligible for promotion.
After a primary is lost, the recovery Writer runs admin recover, the candidate
uses admin accept, the recovery Writer uses admin approve, and the candidate
uses admin commit. The ceremony reuses both out-of-band confirmation codes and
exact-head stale-package checks. It tombstones the old primary, promotes only
the candidate, keeps the recovery authority as an ordinary Writer, and rotates
repository and epoch secrets. Device loss is not cryptographically provable;
this is a trusted break-glass override, so the primary must be taken offline and
the recovery/candidate devices must not edit the vault during the ceremony.
Admin identity backup and restore remain intentionally offline operations and
are the required fallback when no usable recovery authority was configured.
Stop the daemon before running lattice vault admin backup or admin restore.
The backup uses Argon2id and XChaCha20-Poly1305 with a prompted passphrase (or
--password-file) and contains the complete membership state needed to
validate the private signing and encryption keys. It is bound to the configured
vault name and ID. Restore is idempotent for the same state, refuses a healthy
different state, encrypted rollback-floor regression, or pending membership
transition, and requires --replace-corrupt before it preserves and replaces
unreadable membership bytes. Restore therefore also needs the separately
provisioned KDBX password/key file whenever encrypted sync state exists. Backup
creation refuses an existing output path. Provision KDBX credentials, encrypted
sync-state backups, and resource backups separately.
For the system service, review and run the stopped-service operation explicitly:
sudo systemctl stop lattice.servicesudo lattice --system vault admin backup personal \ --out /root/personal.lattice-admin-backup \ --password-file /root/lattice-admin-backup.passwordsudo systemctl start lattice.serviceUse admin restore personal /root/personal.lattice-admin-backup in the same
stopped-service window when restoration is required. Keep the password file and
encrypted backup on separate offline media. Lattice never invokes sudo or
modifies the system service on behalf of the user.
Conflict administration is also live and serialized with sync. Inspect redacted
descriptors with lattice vault conflicts list <name> and show <name> <conflict-id>. Resolve with resolve <name> <conflict-id> --resolution current|conflicting. The daemon pauses mailbox publication, quiesces the vault
actor, commits a signed resolution descending from every conflict head, and
uses the encrypted materialization journal before removing KeePass conflict
artifacts. Exact retries report the already committed head without creating a
new revision.
Retired-secret administration is live and also works while the daemon is stopped. Inspect the redacted retention proof with:
lattice vault secrets report personallattice vault secrets prune personal --dry-runThe dry-run prints a plan ID derived from the membership generation, authoritative head, retained-secret set, local resource envelopes, and cached object bundle. Apply only that exact plan:
lattice vault secrets prune personal --confirm <plan-id>The daemon pauses mailbox work, quiesces the vault actor, recomputes the plan, and rejects stale confirmation before changing encrypted sync state or local membership secrets. Before the first mutation it writes a private local roll-forward journal containing a source fingerprint, the target state, and the approved report. It then persists membership with compare-and-swap, updates revision authority, removes the selected retired payloads from encrypted sync state, fences network sessions, writes a secret-free completion receipt, and removes the journal. Exact retries return that receipt, including when the original client lost its response after commit. Restart and the stopped-daemon path complete a pending journal before importing new KeePass edits or starting network work. A live operation that cannot complete remains quiesced so an exact retry or restart can finish it safely; conflicting membership, recovery, and conflict-resolution mutations are rejected meanwhile. Reports and errors expose identifiers and reasons, never retained key material or conflict plaintext.
Recovery packages may be as large as 320 MiB and are not copied through the
1 MiB IPC frame. The CLI sends the canonical package path and confirmation
code; the daemon opens the package itself. For the hardened system unit, place
the package under a private service-readable path such as
/var/lib/lattice/recovery/. ProtectHome=true hides user home directories,
and PrivateTmp=true means a package in the caller’s /tmp is not visible to
the service. The package must be owned or readable by the lattice service
account and should remain mode 0600.
For a system instance, review and run these privileged staging commands as the administrator:
sudo install -d -o lattice -g lattice -m0700 /var/lib/lattice/recoverysudo install -o lattice -g lattice -m0600 personal.lattice-recovery \ /var/lib/lattice/recovery/personal.lattice-recoverysudo -u lattice lattice --system vault recover import personal \ /var/lib/lattice/recovery/personal.lattice-recovery \ --confirm 0000-0000-0000-0000Retain the staged package until the command reports success so an interrupted
transaction can be retried with the exact same bytes. After success, remove it
with sudo rm /var/lib/lattice/recovery/personal.lattice-recovery.
Vault credentials for unattended startup
Section titled “Vault credentials for unattended startup”Lattice supports password-only, password-plus-key-file, and key-file-only KDBX vaults. Key files are capped at 256 KiB, kept only in process memory while in use, and never copied into membership, pairing packages, or synchronized state. Foreground interactive runs prompt only when a password component is required. Detached and service-manager runs should use noninteractive sources.
Preferred systemd credential naming:
[Service]LoadCredential=lattice-vault-personal.password:/home/alice/.local/share/lattice-secrets/personal.passwordLoadCredential=lattice-vault-personal.key-file:/home/alice/.local/share/lattice-secrets/personal.keyThe files are read from $CREDENTIALS_DIRECTORY/lattice-vault-<vault-name>.password
and $CREDENTIALS_DIRECTORY/lattice-vault-<vault-name>.key-file, with spaces and
unsafe filename characters converted to -. For a vault named Work Vault,
use lattice-vault-Work-Vault.password and lattice-vault-Work-Vault.key-file.
The system unit uses ProtectHome=true, so use LoadCredential for key files
that otherwise live under a user’s home directory.
Portable environment alternatives are also supported:
LATTICE_VAULT_PASSWORD_FILE_PERSONAL=/path/to/personal.password lattice daemon runLATTICE_VAULT_PASSWORD_PERSONAL='test' lattice daemon runLATTICE_VAULT_KEY_FILE_PERSONAL=/path/to/personal.key lattice daemon runThe suffix is the vault name uppercased with non-alphanumeric characters
converted to _. Password file contents may end with a newline; trailing CR/LF
is ignored. Key files are binary and read byte-for-byte. Without an environment
or systemd override, the daemon uses the local key_file path stored by
lattice vault add --key-file.
For multiple registered vaults, Lattice can also keep passwords in a local
encrypted credential cache at $DATA_DIR/lattice/credential-cache.kdbx. The
cache is a normal KDBX file protected by its own password; it is local daemon
state and must not be synced as one of your vaults. Key files are never copied
into this cache. The daemon resolves credentials as follows:
- Load the key file from
LATTICE_VAULT_KEY_FILE_<NAME>, the matching systemd credential, or the configured localkey_filepath, in that order. - Try key-file-only credentials first, so an irrelevant stale cached password cannot lock a key-file-only vault.
- If both components are required, load a password from the per-vault password file, systemd credential, or environment variable; otherwise use the cache entry keyed by vault ID and finally prompt interactively.
Manage the cache with:
lattice vault credential initlattice vault credential set personallattice vault credential listlattice vault credential statuslattice vault credential unlocklattice vault credential remove personalLattice refuses to register a credential cache as a synced vault. If you unregister a vault with lattice vault remove, remove its cached password first with lattice vault credential remove <name> so no stale local secret remains.
The cache master password can be supplied non-interactively with LATTICE_CREDENTIAL_CACHE_PASSWORD_FILE, systemd LoadCredential=lattice-credential-cache.password:/path/to/cache-master.password, or LATTICE_CREDENTIAL_CACHE_PASSWORD. Password file contents may end with a newline; trailing CR/LF is ignored.
If the credential cache exists but the daemon cannot read the cache master during unattended startup, Lattice starts with the affected vaults in locked state instead of prompting or exiting. Unlock the running daemon with:
lattice vault credential unlockThe unlock command reads the cache master from the same noninteractive sources
or prompts interactively, sends it over Lattice’s private runtime IPC socket,
validates it by opening credential-cache.kdbx, and activates vaults whose
passwords are present in the cache. Explicit password files, systemd
credentials, and environment values take precedence over a retained OS copy so
an operator can replace a stale value.
On Linux, the daemon stores only the credential-cache master password in its own process keyring. It is process-local and is released during shutdown. On macOS, Lattice stores a non-synchronizing generic-password item in the user’s default login Keychain. Keychain access is serialized with user interaction disabled: an unavailable or locked Keychain does not display UI from a daemon. Lattice instead tries an explicit password source and otherwise leaves affected vaults locked. The macOS item persists across graceful and forced LaunchAgent restarts so unattended startup remains useful.
The default login Keychain profile is deliberate for compatibility with the standalone CLI and unsigned per-user LaunchAgent. It does not opt into Apple’s entitlement-based data-protection Keychain. Neither platform store replaces the encrypted credential cache on disk. To remove the retained OS copy, stop the daemon and run:
lattice vault credential forgetThe stopped-daemon requirement prevents a running daemon from retaining an already loaded master after the operator believes it was forgotten.
For a systemd user unit, run the unlock command as the same user that owns the
service. For the system unit, run sudo -u lattice lattice --system vault credential unlock; the explicit instance selector targets the service runtime
directory.
Classic Unix daemon mode
Section titled “Classic Unix daemon mode”For Unix hosts without a service supervisor:
lattice daemon run --daemonizeThis detaches the process, writes lattice.pid under the runtime directory, and appends stdout/stderr to lattice.log under the data directory. Stop it with SIGTERM, for example:
kill "$(cat "${XDG_RUNTIME_DIR:-/tmp}/lattice/lattice.pid")"On macOS, the UID-scoped equivalent is:
kill "$(cat "${TMPDIR%/}/lattice-$(id -u)/lattice/lattice.pid")"Classic mode performs the conventional double fork before creating the Tokio
runtime, starts with a 0077 umask, redirects stdin to /dev/null, writes logs
to a private 0600 file, and removes the PID file after graceful shutdown. The
launcher waits for a two-way readiness handshake, so a successful command exit
means the daemon has installed its shutdown signal handlers, acquired the
single-instance lock, and made runtime IPC available. Concurrent classic starts
are serialized by a private startup lock; one starts and the other is rejected.
When running under systemd, launchd, runit, s6, OpenRC, or another supervisor, use foreground mode instead.
macOS LaunchAgent
Section titled “macOS LaunchAgent”The supported macOS service profile is a per-user LaunchAgent. Install the pinned release binary and packaged property list, then bootstrap it into the current graphical user domain:
mkdir -p ~/.local/bininstall -m0755 target/release/lattice ~/.local/bin/latticemkdir -p ~/Library/LaunchAgentsinstall -m0644 packaging/launchd/com.lattice-pm.sync.plist \ ~/Library/LaunchAgents/com.lattice-pm.sync.plistlaunchctl bootstrap "gui/$(id -u)" \ ~/Library/LaunchAgents/com.lattice-pm.sync.plistlaunchctl kickstart -k "gui/$(id -u)/com.lattice-pm.sync"launchctl print "gui/$(id -u)/com.lattice-pm.sync"Stop and remove the loaded agent before replacing its file:
launchctl bootout "gui/$(id -u)/com.lattice-pm.sync"The LaunchAgent uses /bin/sh only to expand the account’s $HOME, then execs
~/.local/bin/lattice daemon run; launchd therefore supervises the foreground
daemon process directly and installation requires no root access. It restarts
only after unsuccessful exits. It uses ~/Library/Application Support/lattice
for configuration and data. Runtime IPC is under the UID-scoped temporary
directory $TMPDIR/lattice-<uid>/lattice, preventing cross-user socket and lock
collisions. A key-file vault normally uses its configured local path; an
operator may instead add LATTICE_VAULT_KEY_FILE_<NAME> with a local path to
the plist’s EnvironmentVariables dictionary. The plist stores only that path,
not key-file bytes. It intentionally does not use classic --daemonize mode.