# How the LNBits Flake Works ## Overview This document explains how the LNBits flake deployment works, particularly how it achieves the equivalent of running `uv run lnbits` on the deployed NixOS machine. ## Architecture The LNBits flake uses `uv2nix` to convert `uv`'s lock file into a reproducible Nix build, creating a Python virtual environment that can be deployed as a NixOS service. ## Key Components ### 1. uv2nix: Converting uv.lock to Nix The flake uses `uv2nix` to read the `uv.lock` file and create a reproducible Nix build: ```nix # Read uv.lock and pyproject.toml workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; # Create overlay preferring wheels (faster than building from source) uvLockedOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; }; ``` This converts the uv-managed dependencies into Nix packages. ### 2. Building a Python Virtual Environment Instead of `uv` creating a venv at runtime, Nix creates one during the build: ```nix # Build venv with all dependencies from uv.lock runtimeVenv = pythonSet.mkVirtualEnv "${projectName}-env" workspace.deps.default; ``` This creates an immutable virtual environment in `/nix/store/...-lnbits-env` with all Python packages installed from the locked dependencies. ### 3. The Wrapper Script (Equivalent to `uv run`) The flake creates a wrapper that mimics `uv run lnbits`: ```nix lnbitsApp = pkgs.writeShellApplication { name = "lnbits"; text = '' export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt export REQUESTS_CA_BUNDLE=$SSL_CERT_FILE export PYTHONPATH="$PWD:${PYTHONPATH:-}" exec ${runtimeVenv}/bin/lnbits "$@" ''; }; ``` This wrapper: - Sets up SSL certificates for HTTPS requests - Adds the current directory to `PYTHONPATH` (so it can find local source files) - Executes the `lnbits` binary from the built venv ### 4. The NixOS Service Module The service module (`lnbits-service.nix`) configures systemd to run LNBits: ```nix # The actual command that runs ExecStart = "${lib.getExe cfg.package} --port ${toString cfg.port} --host ${cfg.host}"; # Environment variables environment = { LNBITS_DATA_FOLDER = "${cfg.stateDir}"; # /var/lib/lnbits LNBITS_EXTENSIONS_PATH = "${cfg.stateDir}/extensions"; LNBITS_PATH = "${cfg.package.src}"; # Points to source } ``` Key points: - `cfg.package` = the venv from the flake (`runtimeVenv`) - `lib.getExe cfg.package` = extracts the executable path: `/nix/store/xxx-lnbits-env/bin/lnbits` - `cfg.package.src` = points back to the LNBits source directory for templates/static files ### 5. Flake Outputs The flake exposes the venv as a package: ```nix packages.default = runtimeVenv; packages.${projectName} = runtimeVenv; # packages.lnbits = runtimeVenv ``` ## How Your Deployment Uses It In your `config/lnbits.nix`: ```nix package = (builtins.getFlake "path:/var/src/lnbits-src").packages.${pkgs.system}.lnbits; ``` This breaks down as: 1. `builtins.getFlake "path:/var/src/lnbits-src"` - Loads the flake from the deployed source 2. `.packages` - Accesses the packages output from the flake 3. `.${pkgs.system}` - Selects the right system architecture (e.g., `x86_64-linux`) 4. `.lnbits` - Gets the `lnbits` package (which equals `runtimeVenv`) ## Understanding Flake References The **flake reference format** is crucial to understanding how this works: ### Local Path Reference ```nix builtins.getFlake "path:/var/src/lnbits-src" ``` - Uses files from the local filesystem at `/var/src/lnbits-src` - The `.src` attribute points to `/var/src/lnbits-src` - Files are mutable - you can edit them - Requires deploying the full source tree via krops ### GitHub Reference ```nix builtins.getFlake "github:lnbits/lnbits/main" ``` - Nix fetches the repository from GitHub - Stores it in `/nix/store/xxx-source/` (read-only) - The `.src` attribute points to `/nix/store/xxx-source` - Files are immutable - No need to deploy source separately ### Comparison | Aspect | `path:/var/src/lnbits-src` | `github:lnbits/lnbits` | |--------|---------------------------|------------------------| | **Source location** | `/var/src/lnbits-src` | `/nix/store/xxx-source` | | **Mutable?** | Yes - can edit files | No - read-only | | **Deployment** | Deploy via krops | Built-in to Nix | | **Updates** | Redeploy source | Change flake ref | | **Local changes** | Supported | Not possible | ## Why Deploy the Full Source? The entire `lnbits` folder must be copied to `/var/src/lnbits-src` because: ### 1. Build Time Requirements The flake needs these files to build the venv: - `flake.nix` - Defines how to build the venv - `uv.lock` - Contains locked dependency versions - `pyproject.toml` - Defines project metadata ### 2. Runtime Requirements LNBits needs the source tree at runtime for: - Python modules in `lnbits/` - HTML templates - Static files (CSS, JavaScript, images) - Extension loading system ## Directory Structure ### On the Deployed Machine ``` /var/src/lnbits-src/ ← Full source deployed by krops ├── flake.nix ← Used to build venv ├── uv.lock ← Used to build venv ├── pyproject.toml ← Used to build venv └── lnbits/ ← Used at runtime ├── templates/ ├── static/ └── ... /nix/store/xxx-lnbits-env/ ← Built venv (Python packages only) ├── bin/lnbits ← Executable └── lib/python3.12/... ← Dependencies ``` ### At Runtime The systemd service: - Runs: `/nix/store/xxx-lnbits-env/bin/lnbits` - With: `LNBITS_PATH=/var/src/lnbits-src` (to find templates/static/etc) - With: `WorkingDirectory=/var/src/lnbits-src` ## Comparison: `uv run` vs Nix Flake ### Traditional `uv run lnbits` ```bash cd /path/to/lnbits uv run lnbits --port 5000 --host 0.0.0.0 ``` This: 1. Reads `uv.lock` 2. Creates/updates a venv in `.venv/` 3. Installs dependencies if needed 4. Runs `lnbits` from the venv 5. Uses current directory for source files ### Nix Flake Approach ```nix package = (builtins.getFlake "path:/var/src/lnbits-src").packages.${pkgs.system}.lnbits; ``` This: 1. ✅ Reads `uv.lock` via `uv2nix` 2. ✅ Creates a venv in `/nix/store` (immutable) 3. ✅ All dependencies are locked and reproducible 4. ✅ Runs `/nix/store/xxx-lnbits-env/bin/lnbits` 5. ✅ Sets `LNBITS_PATH` to source directory for templates/static/extensions 6. ✅ Runs as a systemd service with proper user/permissions 7. ✅ No runtime dependency on `uv` itself ### Key Differences | Aspect | `uv run` | Nix Flake | |--------|----------|-----------| | **Venv location** | `.venv/` in source | `/nix/store/xxx-env` | | **Mutability** | Mutable | Immutable | | **Reproducibility** | Lock file only | Full Nix derivation | | **Service management** | Manual | systemd integration | | **Dependency on uv** | Required at runtime | Only at build time | ## The `.src` Attribute Mystery A common question: where is `cfg.package.src` defined? ### Answer: It's Automatic The `.src` attribute is **not explicitly defined** - it's automatically set by Nix when loading a flake: ```nix # When you do this: builtins.getFlake "path:/var/src/lnbits-src" # Nix automatically: # 1. Reads the flake at /var/src/lnbits-src # 2. Evaluates it and builds outputs # 3. Adds .src = /var/src/lnbits-src to the package ``` This is a built-in Nix flake feature - packages inherit the source location from where the flake was loaded. ## Summary The LNBits flake deployment: 1. **Converts uv dependencies to Nix** using `uv2nix` 2. **Builds an immutable venv** in `/nix/store` 3. **Deploys full source** to `/var/src/lnbits-src` via krops 4. **Loads the flake** from the deployed source using `path:/var/src/lnbits-src` 5. **Runs as a systemd service** with proper environment variables pointing to the source This provides: - ✅ **Reproducibility** - exact same dependencies every time - ✅ **Declarative configuration** - everything in `configuration.nix` - ✅ **Source mutability** - can edit files in `/var/src/lnbits-src` - ✅ **No uv dependency** - service doesn't need `uv` at runtime - ✅ **Proper service management** - systemd integration with user permissions The key insight is that **`path:` vs `github:` in the flake reference** determines whether you use local deployed files or Nix fetches from a remote repository.