An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

docs: add MM-64 design plan — Nix flake + devenv dev shell

Completed brainstorming session. Design includes:
- Minimal flake.nix with nix-systems/default (no flake-parts)
- devenv.nix with languages.rust.toolchainFile + packages list
- rust-toolchain.toml as single source of truth for Rust version
- .envrc for direnv, flake.lock committed
- 2 implementation phases, 13 acceptance criteria

+132
+132
docs/design-plans/2026-03-07-MM-64.md
··· 1 + # MM-64: Nix Flake + devenv Development Shell 2 + 3 + ## Summary 4 + 5 + MM-64 establishes a reproducible development environment for the ezpds Rust workspace using Nix and devenv. The goal is a single command — `nix develop` — that drops any contributor into a shell with the exact same Rust stable toolchain, language tooling (rust-analyzer, clippy, rustfmt), build utilities (just, cargo-audit), and SQLite libraries, regardless of what is already installed on their machine. 6 + 7 + The implementation is intentionally narrow: three configuration files (`flake.nix`, `devenv.nix`, `rust-toolchain.toml`) and two companion files (`.envrc`, `flake.lock`) at the repo root. `rust-toolchain.toml` serves as the single source of truth for the Rust version, making it readable both by devenv inside Nix and by rustup outside it — so contributors who do not use Nix still get the correct toolchain automatically. A Cachix binary cache is wired in via `nixConfig` to avoid 20+ minute cold builds. Build derivations and NixOS system modules are explicitly out of scope; this ticket delivers only the dev shell. 8 + 9 + ## Definition of Done 10 + 11 + A `flake.nix` at the repo root wires devenv into the Nix flake system. A `devenv.nix` configures the development shell. Running `nix develop` drops contributors into a shell with the Rust stable toolchain (rustc, cargo, rust-analyzer, clippy, rustfmt), SQLite dev libraries, `just`, and `cargo-audit`. A `rust-toolchain.toml` pins the toolchain version in a rustup-compatible way. A `.envrc` with `use flake` is committed for direnv users. A `flake.lock` is committed. Build outputs and NixOS modules are explicitly out of scope. 12 + 13 + ## Acceptance Criteria 14 + 15 + ### MM-64.AC1: Dev shell activates 16 + - **MM-64.AC1.1 Success:** `nix develop` completes without error on macOS (aarch64-darwin or x86_64-darwin) 17 + - **MM-64.AC1.2 Success:** `nix develop` completes without error on Linux (x86_64-linux) 18 + 19 + ### MM-64.AC2: Required tools are present in the shell 20 + - **MM-64.AC2.1 Success:** `rustc --version` outputs a stable Rust release (not nightly, not beta) 21 + - **MM-64.AC2.2 Success:** `cargo --version` is available 22 + - **MM-64.AC2.3 Success:** `rust-analyzer --version` is available 23 + - **MM-64.AC2.4 Success:** `cargo clippy --version` is available 24 + - **MM-64.AC2.5 Success:** `rustfmt --version` is available 25 + - **MM-64.AC2.6 Success:** `just --version` is available 26 + - **MM-64.AC2.7 Success:** `cargo audit --version` is available 27 + - **MM-64.AC2.8 Success:** `sqlite3 --version` is available and SQLite dev headers are accessible via `pkg-config` 28 + 29 + ### MM-64.AC3: Rust version is governed by rust-toolchain.toml 30 + - **MM-64.AC3.1 Success:** The Rust version inside the shell matches the `stable` channel as specified in `rust-toolchain.toml` 31 + - **MM-64.AC3.2 Success:** `rustup` on the same machine reads `rust-toolchain.toml` and resolves to the same Rust version without Nix 32 + 33 + ### MM-64.AC4: direnv integration is committed 34 + - **MM-64.AC4.1 Success:** `.envrc` contains `use flake` and is tracked by git (`git ls-files .envrc` returns it) 35 + - **MM-64.AC4.2 Success:** `direnv allow` in repo root activates the shell automatically on `cd` 36 + 37 + ### MM-64.AC5: flake.lock is committed 38 + - **MM-64.AC5.1 Success:** `flake.lock` exists at repo root and is tracked by git 39 + 40 + ### MM-64.AC6: Scope boundaries 41 + - **MM-64.AC6.1 Negative:** `flake.nix` defines no `packages` output (no build derivations) 42 + - **MM-64.AC6.2 Negative:** `flake.nix` defines no `nixosModules` output 43 + 44 + ## Glossary 45 + 46 + - **Nix**: A purely functional package manager that builds software in isolated, reproducible environments. Packages are described as expressions; builds are hermetic and content-addressed. 47 + - **Nix flake**: A standardized Nix project format. A `flake.nix` file declares named *inputs* (dependencies) and *outputs* (shells, packages, modules). A corresponding `flake.lock` pins every input to an exact content hash. 48 + - **flake.lock**: An auto-generated lockfile that records the exact revision and hash of every flake input. Committing it ensures every contributor resolves the same dependency versions. 49 + - **devenv**: A Nix-based tool (from Cachix) for declaring development shell environments. It wraps lower-level Nix plumbing and provides higher-level configuration options like `languages.rust`. 50 + - **devShell**: A Nix flake output type representing a shell environment (`nix develop` enters it). This ticket produces only a `devShells.default` output — no `packages` or `nixosModules`. 51 + - **nixpkgs**: The main package repository for Nix, containing tens of thousands of software packages. devenv uses its own curated fork (`devenv-nixpkgs/rolling`) for compatibility. 52 + - **rust-overlay**: A Nix overlay (provided by `oxalica/rust-overlay`, used internally by devenv) that surfaces rustup toolchain files as Nix derivations. It is how devenv reads `rust-toolchain.toml`. 53 + - **rust-toolchain.toml**: A rustup-standard file that pins a Rust channel, version, components, and targets. Both rustup and rust-overlay/devenv read this file, making it the single source of truth in this design. 54 + - **rustup**: The official Rust toolchain installer and version manager. Reads `rust-toolchain.toml` natively; relevant here because non-Nix contributors rely on it directly. 55 + - **rust-analyzer**: The official Rust language server, used by editors for inline type hints, error checking, and code navigation. 56 + - **clippy**: The official Rust linter (`cargo clippy`). Catches common correctness and style issues beyond what the compiler reports. 57 + - **rustfmt**: The official Rust code formatter (`rustfmt`/`cargo fmt`). 58 + - **just**: A command runner (similar in purpose to `make`) used in this project to define project-level task shortcuts. 59 + - **cargo-audit**: A Cargo subcommand that checks `Cargo.lock` against the RustSec advisory database for known security vulnerabilities in dependencies. 60 + - **SQLite dev libraries**: The C library and header files for SQLite. Required at build time for Rust crates that link against libsqlite3 (e.g., `rusqlite`). `pkg-config` is used by the Rust build system to locate them. 61 + - **pkg-config**: A system tool that reports compiler and linker flags for installed C libraries. Cargo uses it to find native dependencies like SQLite. 62 + - **direnv**: A shell extension that automatically loads (and unloads) environment variables when you `cd` into a directory. The `.envrc` file in a project root contains the hook; `direnv allow` opts the directory in. 63 + - **`.envrc`**: The configuration file read by direnv. In this design it contains a single line, `use flake`, which tells direnv to activate the Nix dev shell automatically. 64 + - **Cachix**: A binary cache hosting service for Nix. Pre-built Nix derivations are fetched from Cachix instead of being compiled locally, dramatically reducing cold-start time. 65 + - **substituter**: Nix terminology for a binary cache source. The `nixConfig.extra-substituters` field in `flake.nix` registers Cachix as a substituter so Nix fetches pre-built outputs rather than building from source. 66 + - **nix-systems**: A small flake (`nix-systems/default`) that provides a canonical list of supported system architectures (e.g., `aarch64-darwin`, `x86_64-linux`). Used here in place of a heavier flake framework like flake-parts. 67 + - **aarch64-darwin / x86_64-darwin / x86_64-linux**: Nix system identifiers. `aarch64-darwin` = Apple Silicon Mac, `x86_64-darwin` = Intel Mac, `x86_64-linux` = 64-bit Linux. Acceptance criteria require the shell to work on all three. 68 + - **devenv.local.nix**: An optional, git-ignored override file that devenv merges with `devenv.nix`. Intended for contributor-local machine-specific configuration. 69 + 70 + ## Architecture 71 + 72 + Minimal Nix flake with devenv as the sole output. Three configuration files govern the dev shell; a fourth enables automatic activation via direnv. 73 + 74 + ``` 75 + repo root/ 76 + ├── flake.nix # Nix entry point — wires devenv, sets Cachix substituter 77 + ├── devenv.nix # Dev shell config — Rust toolchain + packages 78 + ├── rust-toolchain.toml # Toolchain pin — read by both devenv and rustup 79 + ├── .envrc # Direnv hook — `use flake` 80 + └── flake.lock # Input pins — generated by nix develop, committed 81 + ``` 82 + 83 + `flake.nix` has three inputs: `devenv-nixpkgs/rolling` (devenv's curated nixpkgs fork), `cachix/devenv`, and `nix-systems/default` (provides the default system list for multi-arch support without requiring flake-parts). The `nixConfig` block configures Cachix as a binary cache substituter — without it, the first `nix develop` run builds devenv's dependencies from source, which can take 20+ minutes. 84 + 85 + `devenv.nix` uses `languages.rust.toolchainFile = ./rust-toolchain.toml` to wire in the toolchain. devenv does not auto-detect `rust-toolchain.toml`; the explicit path is required. Extra packages (`just`, `cargo-audit`, `sqlite`) go in the `packages` list alongside the Rust config. 86 + 87 + `rust-toolchain.toml` is the single source of truth for the Rust version. devenv reads it via rust-overlay's `fromRustupToolchainFile`. rustup also reads it natively, so contributors not using Nix get the same toolchain. 88 + 89 + ## Existing Patterns 90 + 91 + No prior Nix files exist in the repository. This is a greenfield setup. 92 + 93 + The workspace already has a `.gitignore` with Rust and editor exclusions. This design appends Nix-specific entries (`.devenv/`, `.direnv/`, `devenv.local.nix`) to the existing file rather than replacing it. 94 + 95 + ## Implementation Phases 96 + 97 + <!-- START_PHASE_1 --> 98 + ### Phase 1: Write configuration files 99 + **Goal:** Produce all static configuration files for the dev shell. 100 + 101 + **Components:** 102 + - `flake.nix` — three-input minimal flake with Cachix `nixConfig` and `devenv.lib.mkShell` for `devShells.default` 103 + - `devenv.nix` — `languages.rust.toolchainFile = ./rust-toolchain.toml`, `packages = [pkgs.just pkgs.cargo-audit pkgs.sqlite]` 104 + - `rust-toolchain.toml` — `channel = "stable"`, `components = ["rustfmt" "clippy" "rust-analyzer"]`, `targets = ["aarch64-apple-darwin" "x86_64-unknown-linux-gnu"]` 105 + - `.envrc` — single line: `use flake` 106 + - `.gitignore` — append `.devenv/`, `.direnv/`, `devenv.local.nix` 107 + 108 + **Dependencies:** None (first phase) 109 + 110 + **Done when:** All five files exist at repo root with correct contents; `.gitignore` includes the three Nix entries 111 + <!-- END_PHASE_1 --> 112 + 113 + <!-- START_PHASE_2 --> 114 + ### Phase 2: Generate flake.lock and commit 115 + **Goal:** Run `nix develop` to resolve and pin all flake inputs, then commit everything. 116 + 117 + **Components:** 118 + - `flake.lock` — generated by running `nix develop` for the first time 119 + - Git commit — all six files (`flake.nix`, `devenv.nix`, `rust-toolchain.toml`, `.envrc`, `.gitignore`, `flake.lock`) committed together 120 + 121 + **Dependencies:** Phase 1 (all configuration files present) 122 + 123 + **Done when:** `nix develop` exits successfully; `flake.lock` exists and is committed; all required tools (`rustc`, `cargo`, `rust-analyzer`, `clippy`, `rustfmt`, `just`, `cargo-audit`, `sqlite3`) are available inside the shell 124 + <!-- END_PHASE_2 --> 125 + 126 + ## Additional Considerations 127 + 128 + **Non-Nix contributors:** `rust-toolchain.toml` is read by rustup directly. Contributors who install Rust via rustup outside Nix will automatically get the same stable channel and components. 129 + 130 + **Cachix key rotation:** The `extra-trusted-public-keys` value in `flake.nix` is devenv's current public key. If Cachix rotates this key, the first `nix develop` after rotation will fall back to building from source. Check devenv's docs for the updated key when upgrading devenv. 131 + 132 + **`devenv.local.nix`:** Excluded from `.gitignore` by convention. Contributors can use this file for machine-specific overrides without affecting the shared config.