beyond-the-prompt/docs/adr/002-nix-flake-architecture.md
Scott Scoble d2da71e53a Initial commit: Beyond the Prompt workshop presentation by Scott Scoble
- Reveal.js presentation on Skills and Subagents pattern
- Nix flake with Caddy-based HTTP server and process-compose orchestration
- ADRs documenting architectural decisions
- Skills for AI implementation guidance
- Full Scott Scoble author attribution in README
2026-05-23 18:19:14 -07:00

6.9 KiB

ADR 002: Nix Flake Architecture

Status

Proposed

Context

The project needs a Nix flake that:

  • Provides reproducible development environment
  • Defines two runnable apps: present and demo
  • Supports multiple platforms (Linux and macOS, both x64 and ARM)
  • Minimizes complexity while providing good developer experience
  • Follows Nix best practices

Decision

Implement a multi-platform flake with forAllSystems helper, separate apps for presentation and demo, and a comprehensive devShell.

Architecture

System Support

supportedSystems = [
  "x86_64-linux"
  "aarch64-linux"
  "x86_64-darwin"
  "aarch64-darwin"
];

Core Outputs

  1. apps.present

    • Type: app
    • Function: Serve presentation on http://localhost:8000
    • Implementation: process-compose orchestrating Caddy server
    • Configuration: Nix-generated Caddyfile and process-compose.yaml
  2. apps.demo

    • Type: app
    • Function: Execute demonstration script
    • Implementation: process-compose orchestrating demo processes
    • Configuration: Nix-generated process-compose.yaml for demo services
  3. devShells.default

    • Purpose: Development environment with all necessary tools
    • Contents: bash, caddy, process-compose, and optional tools (asciinema, etc.)
    • Shell hook: Display helpful usage instructions

Rationale

Multi-Platform with forAllSystems

Why: Developers use various platforms; presentation should work everywhere How: Use nixpkgs.lib.genAttrs to generate outputs for all systems Alternative: Single-platform support would exclude contributors

Separate Apps vs Unified Interface

Decision: Two separate apps (present and demo) Why:

  • Clear separation of concerns
  • Can run independently
  • Better UX (nix run .#present is more explicit than nix run . -- present)

Alternative considered: Single app with subcommands

  • Would require argument parsing
  • More complex implementation
  • Less idiomatic for Nix

writeScript vs Packages

Decision: Use pkgs.writeScript for app wrappers Why:

  • Simple bash scripts don't need full package machinery
  • Reduces boilerplate
  • Direct access to flake's self reference
  • Easier to maintain

Alternative: Proper derivations with stdenv.mkDerivation

  • Overkill for simple wrappers
  • More complex to reference source files

HTTP Server and Process Management

Decision: Use Caddy for HTTP serving, orchestrated by process-compose

Architecture:

  • Caddy: Modern HTTP server with automatic HTTPS, JSON API, config reloading
  • process-compose: Process orchestrator (alternative to docker-compose for processes)
  • Configuration: Generated via Nix expressions, no manual config files

Why Caddy:

  • Configuration as code via Nix
  • JSON/Caddyfile generation from Nix expressions
  • Powerful features (reverse proxy, file server, API)
  • Good developer experience
  • Available in nixpkgs

Why process-compose:

  • Orchestrates multiple services (presentation server, potential demo services)
  • YAML config generated from Nix
  • Terminal UI for monitoring processes
  • Graceful shutdown and dependency management
  • Better than ad-hoc bash scripts for multi-process apps

Configuration Strategy:

# Generate Caddyfile
caddyConfig = pkgs.writeText "Caddyfile" ''
  :8000 {
    root * ${self}/presentation
    file_server
    encode gzip
  }
'';

# Generate process-compose config
processComposeConfig = pkgs.writeText "process-compose.yaml" (builtins.toJSON {
  processes = {
    presentation = {
      command = "${pkgs.caddy}/bin/caddy run --config ${caddyConfig}";
      readiness_probe = {
        http_get = {
          host = "localhost";
          port = 8000;
        };
      };
    };
  };
});

DevShell Philosophy

Decision: Include common tools, comment optional ones Why:

  • Balance between "batteries included" and minimalism
  • Comments serve as documentation
  • Easy to uncomment tools as needed
  • Shell hook provides guidance

Implementation Details

Flake Structure

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      supportedSystems = [ ... ];
      forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems
        (system: f system (import nixpkgs { inherit system; }));
    in {
      apps = forAllSystems (system: pkgs: { ... });
      devShells = forAllSystems (system: pkgs: { ... });
    };
}

App Implementation Pattern

present = {
  type = "app";
  program = "${pkgs.writeScript "present" ''
    #!/usr/bin/env bash
    exec ${pkgs.process-compose}/bin/process-compose \
      up -f ${processComposeConfig}
  ''}";
};

Configuration File Generation

Caddy Configuration (Caddyfile):

caddyConfig = pkgs.writeText "Caddyfile" ''
  :8000 {
    root * ${self}/presentation
    file_server browse
    encode gzip
    log {
      output stdout
      format console
    }
  }
'';

process-compose Configuration (YAML via JSON):

# YAML generation using builtins.toJSON for structured config
processComposeConfig = pkgs.writeText "process-compose.yaml" (
  builtins.toJSON {
    version = "0.5";
    processes = {
      caddy = {
        command = "${pkgs.caddy}/bin/caddy run --config ${caddyConfig}";
        readiness_probe = {
          http_get = {
            host = "localhost";
            port = 8000;
          };
        };
      };
    };
  }
);

Source File References

  • Presentation: ${self}/presentation
  • Demo files: ${self}/demo/
  • Configs are generated, not stored in repo

Consequences

Positive

  • Reproducible across all major platforms
  • Configuration as code (no manual config files)
  • Process orchestration with monitoring UI
  • Clean separation: presentation vs demo
  • Graceful shutdown and process management
  • Readiness probes ensure services are up
  • All dependencies from nixpkgs

Negative

  • process-compose adds complexity vs simple HTTP server
  • Files copied to Nix store (not live-reloaded during development)
  • Need to re-run nix run after file changes
  • YAML generation via JSON (no native YAML in Nix)

Mitigation

  • Complexity justified by better orchestration and monitoring
  • For rapid iteration, use devShell with direct commands:
    nix develop
    # Then run Caddy directly for live development:
    caddy run --config <(echo ':8000 { root * ./presentation; file_server }')
    
  • process-compose TUI provides visibility into running services
  • JSON→YAML conversion is transparent and well-tested

Future Considerations

  • Add checks output for presentation validation (broken links, etc.)
  • Add packages output for static site generation
  • Consider nix flake init template for similar presentations
  • Add formatter (e.g., alejandra) to devShell
  • Explore Caddy admin API for dynamic config updates
  • Add hot-reload capability using Caddy's config API
  • Consider additional process-compose processes for demo infrastructure