Skip to content

Running Lattice as a Daemon

Lattice now has an explicit daemon entrypoint:

Terminal window
lattice daemon run

lattice sync still runs the same foreground daemon for compatibility.

Under systemd, run Lattice in the foreground and let systemd supervise the process. Do not use --daemonize from a unit file.

System unit:

Terminal window
sudo install -Dm0644 packaging/systemd/lattice.sysusers.conf /usr/lib/sysusers.d/lattice.conf
sudo systemd-sysusers /usr/lib/sysusers.d/lattice.conf
sudo install -Dm0644 packaging/systemd/lattice.tmpfiles.conf /usr/lib/tmpfiles.d/lattice.conf
sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/lattice.conf
sudo install -Dm0644 packaging/systemd/lattice.service /etc/systemd/system/lattice.service
sudo systemctl daemon-reload
sudo systemctl enable --now lattice.service
journalctl -u lattice.service -f

User unit:

Terminal window
install -Dm0755 target/release/lattice ~/.local/bin/lattice
install -Dm0644 packaging/systemd/user/lattice.service ~/.config/systemd/user/lattice.service
systemctl --user daemon-reload
systemctl --user enable --now lattice.service
journalctl --user -u lattice.service -f

For login-independent user services, enable lingering separately:

Terminal window
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:

Terminal window
sudo -u lattice lattice --system vault list
sudo -u lattice lattice --system status
sudo -u lattice lattice --system vault credential unlock

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

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:

Terminal window
lattice daemon reload

Invite 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:

Terminal window
sudo systemctl stop lattice.service
sudo lattice --system vault admin backup personal \
--out /root/personal.lattice-admin-backup \
--password-file /root/lattice-admin-backup.password
sudo systemctl start lattice.service

Use 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:

Terminal window
lattice vault secrets report personal
lattice vault secrets prune personal --dry-run

The 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:

Terminal window
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:

Terminal window
sudo install -d -o lattice -g lattice -m0700 /var/lib/lattice/recovery
sudo install -o lattice -g lattice -m0600 personal.lattice-recovery \
/var/lib/lattice/recovery/personal.lattice-recovery
sudo -u lattice lattice --system vault recover import personal \
/var/lib/lattice/recovery/personal.lattice-recovery \
--confirm 0000-0000-0000-0000

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

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.password
LoadCredential=lattice-vault-personal.key-file:/home/alice/.local/share/lattice-secrets/personal.key

The 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:

Terminal window
LATTICE_VAULT_PASSWORD_FILE_PERSONAL=/path/to/personal.password lattice daemon run
LATTICE_VAULT_PASSWORD_PERSONAL='test' lattice daemon run
LATTICE_VAULT_KEY_FILE_PERSONAL=/path/to/personal.key lattice daemon run

The 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:

  1. Load the key file from LATTICE_VAULT_KEY_FILE_<NAME>, the matching systemd credential, or the configured local key_file path, in that order.
  2. Try key-file-only credentials first, so an irrelevant stale cached password cannot lock a key-file-only vault.
  3. 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:

Terminal window
lattice vault credential init
lattice vault credential set personal
lattice vault credential list
lattice vault credential status
lattice vault credential unlock
lattice vault credential remove personal

Lattice 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:

Terminal window
lattice vault credential unlock

The 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:

Terminal window
lattice vault credential forget

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

For Unix hosts without a service supervisor:

Terminal window
lattice daemon run --daemonize

This 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:

Terminal window
kill "$(cat "${XDG_RUNTIME_DIR:-/tmp}/lattice/lattice.pid")"

On macOS, the UID-scoped equivalent is:

Terminal window
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.

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:

Terminal window
mkdir -p ~/.local/bin
install -m0755 target/release/lattice ~/.local/bin/lattice
mkdir -p ~/Library/LaunchAgents
install -m0644 packaging/launchd/com.lattice-pm.sync.plist \
~/Library/LaunchAgents/com.lattice-pm.sync.plist
launchctl bootstrap "gui/$(id -u)" \
~/Library/LaunchAgents/com.lattice-pm.sync.plist
launchctl 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:

Terminal window
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.