Installation
Install Aperture with Nix as a per-user service, configure it, and run it on NixOS or any Linux host with user systemd.
Aperture is packaged with Nix and runs as a per-user desktop service on NixOS (or any Linux host with user systemd).
For the single-container deployment, see Docker. For local source development, see Development.
Build and install
From the repository:
nix build .#aperture
./result/bin/aperture --helpInstall into your user profile:
nix profile install .#apertureThe package provides:
aperture— main supervisor CLI and HTTP APIaperture-mount-session/aperture-unmount-session— privileged overlay helpers (sudo)browser-session-wrapper— bwrap Chromium launcher used by user systemd units- systemd user units under
$out/share/systemd/user/(Nix fixup may also link$out/lib/systemd/user/) - Traefik static config template under
$out/share/aperture/traefik/ - sudoers snippet under
$out/share/aperture/sudoers/
Configuration
Create ~/.config/aperture/aperture.yaml (or set APERTURE_* environment variables). Required values include:
store_root,runtime_root— persistent and runtime state directoriesexternal_base_url— public URL Traefik serves (for generated connection and signed-file links)embed_allowed_origins— optional origins of other web apps that embed shared sessions (see Session sharing)- MCP settings — enabled by default; see the example below
channels— browser channel registry (executablepaths only; API accepts channel names)
Example:
store_root: /home/USER/.local/state/aperture
runtime_root: /run/user/UID/aperture
external_base_url: https://browser.example.test
listen_address: 127.0.0.1:8080
mcp_enabled: true
browser_tools_default: core,vision,network
tool_output_max_bytes: 16777216
signed_file_url_ttl: 15m
signed_file_url_max_ttl: 24h
login_methods: [password, api_token, passkey, oidc]
web_session_lifetime: 720h
web_session_idle_timeout: 24h
channels:
chromium:
executable: /run/current-system/sw/bin/chromium
default_args: []The first available login_methods entry is the default in the login dialog. password and api_token show forms; passkey starts WebAuthn; oidc expands to the providers below in their configured order.
To enable OIDC login, register ${external_base_url}/auth/oidc/PROVIDER_ID/callback with the provider and add it to the config:
oidc_providers:
- id: company
display_name: Company account
issuer_url: https://accounts.example.test
client_id: aperture
client_secret: REPLACE_ME
scopes: [openid, profile, email]
auto_provision: falseWith auto_provision: false, a system administrator must create the user first. Aperture links the provider identity only when the provider returns a verified email matching that user. Set auto_provision: true to create users on first login. Tenant access still comes from the user's tenant memberships.
Authenticated users can register discoverable passkeys from the web UI. Aperture derives the WebAuthn RP ID and origin from external_base_url, so changing its hostname makes credentials registered for the old RP unavailable. Passkey registration requires an existing account session; passkeys do not provision users.
Users with an email address can set an Argon2id password from the web UI. They can also enable TOTP for password login; enrollment creates ten one-time recovery codes that are shown once and stored only as SHA-256 hashes. OIDC and passkey login remain available when TOTP is enabled.
The central Streamable HTTP MCP endpoint is /mcp; the per-session endpoint is /sessions/:sessionId/mcp. Central MCP requires an Aperture API bearer token (apt_<tokenId>_<secret>). Per-session MCP accepts an authorized API bearer token or the owner-only sessionToken (aps_<sessionId>_<secret>). Share links instead use editor (ape_) and viewer (apv_) capabilities with narrower live-session access; they do not authorize MCP, files, recordings, or /api/*. Signed session-file URLs use apf_<payload>.<signature> query tokens. OIDC, passkey, and password browser sessions authorize the web UI and same-origin API or live-session requests, but not MCP clients.
On first aperture serve, the database is migrated and a local job token is created at $runtime_root/job-token (mode 0600). The GC timer uses aperture trigger gc, which reads that token and calls POST /internal/jobs/gc on the loopback listener.
User systemd services
Copy or link units from the package into ~/.config/systemd/user/ (the NixOS/Home Manager module does this automatically). Enable the core services:
systemctl --user daemon-reload
systemctl --user enable --now aperture@blue.service
systemctl --user enable --now aperture-gc.timer
systemctl --user enable --now aperture-traefik.servicebrowser-session@.service is started by Aperture per session; do not enable it directly.
Prepare Traefik runtime config before starting aperture-traefik.service:
mkdir -p "$XDG_RUNTIME_DIR/aperture/traefik"
cp "$(dirname "$(which aperture)")/../share/aperture/traefik/static.yaml.template" \
"$XDG_RUNTIME_DIR/aperture/traefik/static.yaml"
# Edit static.yaml: set entrypoints, certificates, and dynamic config watch path to
# $XDG_RUNTIME_DIR/aperture/traefik/dynamic.yaml (written by Aperture).Sudo helpers
Install the sudoers fragment as root (path from the package output):
sudo install -m 0440 share/aperture/sudoers/aperture-mount-helpers /etc/sudoers.d/aperture-mount-helpersEdit the file to substitute your login user for %aperture_user% and the store-path helper binaries for @mountSessionHelper@ / @unmountSessionHelper@.
Bootstrap and operation
aperture admin provision --admin-token-file ./admin-tokenThe command migrates the database, creates a default tenant when no active tenant exists, and writes the initial system-admin token with mode 0600. It is safe to run again; existing tokens and tenants are left unchanged.
Garbage collection is not scheduled inside aperture serve; enable aperture-gc.timer or run aperture trigger gc manually.
NixOS module
The flake exposes nixosModules.aperture at the flake root (not per-system). It configures a single login user with:
- the Aperture package
- generated
/etc/aperture/aperture.tomlsettings, or an external root-owned config file - blue/green API, browser session, optional Traefik, and garbage collection user units
- passwordless sudo for the packaged mount/unmount helpers
- the
aperture-rollouthealth-checked blue/green deployment helper
Import the module and set the required options:
imports = [ inputs.aperture.nixosModules.aperture ];
services.aperture = {
enable = true;
user = "alice";
externalBaseUrl = "https://browser.example.test";
storeRoot = "/srv/aperture";
settings = {
log_level = "info";
webrtc_media_mode = "auto";
webrtc_compositor_enabled = true;
};
environmentFile = "/home/alice/.config/aperture/aperture.env";
traefik.enable = true;
};settings accepts non-secret Aperture TOML values. The module owns storage, runtime, deployment, public URL, and default Chromium channel fields so dedicated module options remain authoritative. Use configFile to replace the generated config entirely.
Keep credentials out of settings, because generated Nix files are readable from the Nix store. Put environment overrides in the file selected by environmentFile, with permissions limited to the Aperture user. For example:
APERTURE_WEBRTC_ICE_SERVERS='[{"urls":["turn:turn.example.test:3478"],"username":"aperture","credential":"..."}]'The deployment defaults are:
- blue API:
127.0.0.1:28080 - green API:
127.0.0.1:28082 - drain time: 30 seconds
- candidate health timeout: 30 seconds
Enable the edge proxy with services.aperture.traefik.enable = true; its default entrypoint is 127.0.0.1:28081. TLS and external ingress remain outside the module.
On the first activation, reload the user manager, start blue, and make that instance persistent:
systemctl --user daemon-reload
systemctl --user start aperture@blue.service
mkdir -p ~/.config/systemd/user/default.target.wants
rm -f ~/.config/systemd/user/aperture@blue.service
ln -sfn /etc/systemd/user/aperture@.service \
~/.config/systemd/user/default.target.wants/aperture@blue.service
systemctl --user daemon-reloadFor subsequent NixOS generations, reload the user manager and run aperture-rollout. The helper starts the inactive color, waits for /api/health, updates deployment state and Traefik routing, waits for the configured drain period, then stops the old API. A failed health check leaves the active color unchanged; a cutover error starts and restores the previous color.
Verification
nix flake checkLive desktop smoke tests exercise the real app.Serve path, user systemd browser units, sudo-backed overlay mounts, and direct loopback CDP. They are gated behind APERTURE_LIVE_E2E=1 and do not start Traefik. Traefik/CDP edge routing is covered separately by APERTURE_LIVE_TRAEFIK=1 in internal/traefik/live_test.go.