Skip to content

feat: seed nix-happier with full Nix packaging#1

Open
das-monki wants to merge 19 commits intohappier-dev:mainfrom
das-monki:main
Open

feat: seed nix-happier with full Nix packaging#1
das-monki wants to merge 19 commits intohappier-dev:mainfrom
das-monki:main

Conversation

@das-monki
Copy link

@das-monki das-monki commented Mar 4, 2026

Summary

Initial content for the nix-happier repo — a Nix flake that builds and deploys Happier Server and CLI.

  • Flake structure using flake-parts with per-system outputs (packages, checks, devshell, apps)
  • Packages: happier-server and happier-cli built from the happier monorepo source
  • Web UI: happier-ui-web derivation (Expo static export) bundled with the server via HAPPIER_SERVER_UI_DIR
  • NixOS module (services.happier-server) supporting full mode (PostgreSQL + Redis + MinIO) and light mode (SQLite-only)
  • Prisma engines: prebuilt binaries fetched by hash, with nix run .#update-prisma-hashes for updates
  • CI: GitHub Actions for x86_64-linux (full VM test), aarch64-linux (native ARM runner), aarch64-darwin
  • Examples: light mode and full mode (Tailscale + Caddy TLS) configurations
  • Linting: deadnix + statix via nix flake check

Test plan

  • nix fmt passes
  • nix flake check passes (deadnix, statix, VM integration test on x86_64-linux)
  • nix build .#happier-server succeeds with bundled web UI
  • nix build .#happier-cli succeeds
  • CI passes on all three runners
  • Deploy and verify server + web UI

Summary by CodeRabbit

  • Documentation

    • Added comprehensive README guide covering project scope, quick start, and deployment modes.
    • Added configuration guidelines and examples for AI agents.
  • New Features

    • Packaged happier-server and happier-cli for distribution across multiple platforms.
    • Added NixOS module enabling easy service deployment with light and full modes.
    • Provided example configurations for various deployment scenarios (light, Tailscale, full-stack).
  • CI/CD

    • Added automated Nix-based build workflow across Linux and macOS platforms.
  • Tests

    • Integrated NixOS VM tests for service validation.
    • Added static code linting checks.

das-monki and others added 19 commits March 4, 2026 21:23
Port all Nix expressions (flake, packages, NixOS module, devshell,
prisma-engines-prebuilt) into a standalone repo. The happier monorepo
source is now a non-flake input so packages build from the upstream
source tree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Standalone Nix repo doesn't need the nix/ prefix — move modules/,
packages/, and scripts/ to the repo root.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follow Blueprint/sops-nix convention:
- modules/nixos/ for exported NixOS modules (future: darwin/, home/)
- devshell.nix and packages.nix at root (internal flake-parts wiring)
- packages/ for helper derivations (prisma-engines-prebuilt)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. nix fmt uses treefmt which needs --fail-on-change, not --check
2. autoPatchelfHook is Linux-only — skip it on darwin where Prisma
   ships self-contained Mach-O binaries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch CI to idiomatic `nix flake check`, which builds all packages,
runs linting (deadnix, statix), and on Linux runs a NixOS VM integration
test for the happier-server light mode.

Pin the happier input to a release tag and expose `nix run .#update-happier`
as a flake app for updating it. Add automated workflows for happier release
tracking (daily) and general input updates via update-flake-lock (weekly).
Happier is still in preview with no stable releases. Revert to following
the default branch and remove the automated update workflows and flake
app. These are preserved on the ci/auto-update-workflows branch for when
official releases begin.
Package the hash updater as `apps.update-prisma-hashes` via
writeShellApplication, and add `apps.update` that runs
`nix flake update` followed by the hash refresh. Yarn.lock is
resolved from the `happier` flake input at eval time instead of
assuming a sibling directory. Uses `nix store prefetch-file` for
native SRI hashes, replacing legacy `nix-prefetch-url` (base32).

- Convert all 6 platform hashes to SRI format
- Add `update` command to devshell
- Delete scripts/update-prisma-hashes.sh
Move the happier-server light mode config into examples/ so it serves
as both runnable documentation and the actual config consumed by CI.
Add README.md covering flake outputs, quick start, module options,
examples, development, and repo structure. Remove yarn-based devshell
commands and packages that were carried over from the happier monorepo
and don't work in this repo.
Provides AI coding agents with repo context, Nix style conventions,
key patterns (flake-parts, NixOS module, Prisma engine updates),
verification steps, and do's/don'ts. CLAUDE.md symlinks to AGENTS.md
so both Claude Code and other tools pick up the same instructions.
Add examples/happier-server-tailscale.nix showing the recommended
production setup: light mode + Tailscale + nginx TLS reverse proxy
with auto-renewed certs. Add a Secrets section to the README making
the HANDY_MASTER_SECRET environment file requirement prominent.
Restructure Quick Start to lead with the Tailscale example.
Caddy natively recognizes Tailscale URLs and auto-provisions TLS certs,
eliminating the need for manual cert generation, timers, and permission
fixups. This reduces the Tailscale example from ~60 lines to ~20.
Replace QEMU user-space emulation with GitHub's native ubuntu-24.04-arm
runner. The NixOS VM integration test is skipped on aarch64 since GitHub
ARM runners lack KVM; it continues to run in the x86_64-linux job.
Production-ready config showing PostgreSQL + Redis + MinIO with agenix
secret management and Tailscale TLS via Caddy reverse proxy.
Add happier-ui-web derivation that builds the Expo web bundle as a
separate package (following the lldap/Immich pattern from nixpkgs).
The server wrappers set HAPPIER_SERVER_UI_DIR so the web UI is served
automatically. The UI is also exposed via passthru.web for overrides.
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive Nix flake-based infrastructure for the nix-happier project, including CI/CD workflows, a NixOS module for deploying Happier Server with optional services (PostgreSQL, Redis, MinIO), multi-mode example configurations, development environment setup, and packaging scripts for CLI and server components with Prisma integration support.

Changes

Cohort / File(s) Summary
GitHub Actions & CI/CD
.github/workflows/nix-build.yml
New workflow with triggers for pull_request and push, testing Nix flakes across x86_64-linux, aarch64-darwin, and aarch64-linux with formatting and lint checks.
Documentation
AGENTS.md, CLAUDE.md, README.md
Added comprehensive guides for AI agents, project overview, quick start, mode configurations, examples, development workflow, and repository structure.
Flake Core Configuration
flake.nix, packages.nix, packages/prisma-engines-prebuilt.nix
Main flake entry point with multi-system support, inputs, and apps for updating Prisma hashes and dependencies. Packaging configuration for happier-cli and happier-server derivations with pre-built Prisma engine binary fetching.
Development & Testing Environment
devshell.nix, checks.nix, statix.toml
Dev shell with formatting and update commands; linting checks (deadnix, statix) and NixOS VM integration test for server validation.
NixOS Module
modules/nixos/happier-server.nix
Comprehensive module exposing services.happier-server configuration with mode-aware orchestration of PostgreSQL, Redis, MinIO, database migrations, and service hardening.
Example Configurations
examples/happier-server-light.nix, examples/happier-server-tailscale.nix, examples/happier-server-full.nix
Three example deployments: minimal light mode for CI, light mode with Tailscale/Caddy TLS, and full mode with PostgreSQL, Redis, MinIO, and TLS reverse proxy.

Sequence Diagram(s)

sequenceDiagram
    participant NixOS as NixOS System
    participant Systemd as systemd
    participant Migrate as Migration Service
    participant MinIO as MinIO Service
    participant BucketInit as Bucket Init Service
    participant Server as Happier Server
    participant DB as PostgreSQL
    participant Redis as Redis

    NixOS->>Systemd: Start full mode services
    Systemd->>DB: Start PostgreSQL (if createLocally)
    Systemd->>Redis: Start Redis (if createLocally)
    Systemd->>MinIO: Start MinIO (if createLocally)
    MinIO-->>Systemd: Ready signal
    Systemd->>BucketInit: Trigger bucket initializer
    BucketInit->>MinIO: Create configured bucket
    BucketInit-->>Systemd: Complete
    Systemd->>Migrate: Run DB migrations
    Migrate->>DB: Execute migration scripts
    DB-->>Migrate: Schema ready
    Migrate-->>Systemd: Complete
    Systemd->>Server: Start happier-server (full)
    Server->>DB: Connect to PostgreSQL
    Server->>Redis: Connect to Redis cache
    Server->>MinIO: Connect to object storage
    Server-->>Systemd: Running
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Poem

🐰 A flake so fine, with systems galore,
PostgreSQL, Redis, and so much more,
Migrations dance, services align,
From light to full, a mode divine,
NixOS modules keep order in sight! 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main objective: seeding the nix-happier repository with comprehensive Nix packaging infrastructure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (3)
packages.nix (2)

319-320: Avoid masking server typecheck failures in package builds.

Swallowing tsc --noEmit failures hides regressions in the packaged server path.

Stricter default (with optional escape hatch if needed)
-            (cd apps/server && node ../../node_modules/typescript/bin/tsc --noEmit) || echo "WARN: tsc --noEmit had errors (non-fatal for tsx runtime)"
+            (cd apps/server && node ../../node_modules/typescript/bin/tsc --noEmit)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages.nix` around lines 319 - 320, The packaging step currently masks
TypeScript typecheck failures by appending '|| echo "WARN: tsc --noEmit had
errors..."' to the tsc --noEmit invocation; remove the unconditional swallow and
change the line so that tsc --noEmit fails the build by default, but allow an
explicit opt-out via an environment flag (e.g., SKIP_SERVER_TYPECHECK=true)
which, when set, runs tsc --noEmit and prints a clear warning if it fails;
specifically replace the current command string containing "node
../../node_modules/typescript/bin/tsc --noEmit || echo ..." so that failures
propagate unless SKIP_SERVER_TYPECHECK is set and handled with an if/else that
logs the warning.

164-167: Consider adding --input-type=module for explicit ESM evaluation.

While Node.js v22+ automatically detects and retries ESM syntax when CommonJS fails, explicitly declaring module mode avoids this retry overhead and clarifies intent.

Suggested improvement
-            node -e "
+            node --input-type=module -e "
               const { syncBundledWorkspaceDist } = await import('./apps/cli/scripts/buildSharedDeps.mjs');
               syncBundledWorkspaceDist({ repoRoot: process.cwd() });
             "
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages.nix` around lines 164 - 167, The inline Node invocation that imports
'./apps/cli/scripts/buildSharedDeps.mjs' and calls syncBundledWorkspaceDist
should explicitly run Node in ESM mode; update the node command that currently
runs the eval block invoking syncBundledWorkspaceDist to include the flag
--input-type=module so the import('./...buildSharedDeps.mjs') is evaluated as
ESM (refer to the node -e invocation and the syncBundledWorkspaceDist
import/call).
flake.nix (1)

116-138: Add a preflight count check before positional hash replacement.

The script assumes exactly six hashes; if layout changes, replacements can drift silently.

Preflight guard for deterministic updates
                   # Read current hashes in order of appearance
                   mapfile -t CURRENT_HASHES < <(grep -oP 'Hash = "\K[^"]+' "$NIX_FILE")
+                  if [ "${`#CURRENT_HASHES`[@]}" -ne 6 ]; then
+                    echo "ERROR: Expected 6 hashes in $NIX_FILE, found ${`#CURRENT_HASHES`[@]}" >&2
+                    exit 1
+                  fi
@@
                   for i in "''${!CURRENT_HASHES[@]}"; do
                     old="''${CURRENT_HASHES[$i]}"
                     new="''${ORDERED_NEW[$i]}"
+                    if [ -z "$new" ]; then
+                      echo "ERROR: Missing new hash for index $i" >&2
+                      exit 1
+                    fi
                     if [ "$old" != "$new" ]; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 116 - 138, Add a preflight guard before the
replacement loop to ensure the script has exactly six hashes and the expected
NEW_HASHES keys so replacements cannot drift: check the length of CURRENT_HASHES
(using ${`#CURRENT_HASHES`[@]}) equals 6 and verify each required NEW_HASHES key
exists (aarch64-linux_qe, aarch64-linux_se, aarch64-darwin_qe,
aarch64-darwin_se, x86_64-linux_qe, x86_64-linux_se); if the check fails, print
a clear error mentioning NIX_FILE and abort (exit 1) instead of proceeding to
the loop that uses CURRENT_HASHES, ORDERED_NEW and the for loop over
${!CURRENT_HASHES[@]}.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/nix-build.yml:
- Line 20: Update mutable action refs to immutable commit SHAs: replace uses:
actions/checkout@v4 with uses: actions/checkout@<commit-sha> and replace
DeterminateSystems/nix-installer-action@main with
DeterminateSystems/nix-installer-action@<commit-sha>; do this for every
occurrence of those two action refs in the workflow file. Locate the exact
commit SHAs from each action's upstream GitHub repo (releases/tags or the commit
you want to pin), then substitute the tag/branch with that SHA to ensure
reproducible CI.

In `@AGENTS.md`:
- Around line 11-20: The fenced code block in AGENTS.md (the repo layout block
listing flake.nix, packages.nix, etc.) is missing a language tag; update the
opening fence from ``` to ```text so the block becomes a ```text fenced code
block to satisfy markdownlint MD040.

In `@checks.nix`:
- Around line 50-52: The health check currently treats any digit string
(including curl's "000" on transport failure) as success; update the
server.succeed fallback so the curl -w '%{http_code}' output is validated
against a real HTTP status range (e.g. match three-digit codes 100–599) instead
of any digit string. Locate the server.succeed invocation that runs the two-curl
command (the line calling server.succeed with the fallback curl -w
'%{http_code}') and replace the grep pattern to only accept valid HTTP status
codes (for example, use a regex that matches ^[1-5][0-9]{2}$ or ^[1-5][0-9]{2}$
to exclude "000"). Ensure the primary -sf curl remains to succeed on normal
responses and the fallback only treats real HTTP codes as success.

In `@examples/happier-server-full.nix`:
- Around line 24-32: The example is missing the required
services.happier-server.package attribute (modules/nixos/happier-server.nix
defines package with no default); update the example to include a package value
for services.happier-server (or add a comment making it explicit that callers
must supply services.happier-server.package), e.g. reference the appropriate
derivation from your pkgs set or the same package used in the light example so
the module receives a concrete package value.

In `@examples/happier-server-tailscale.nix`:
- Around line 19-26: The example enables services.happier-server but omits the
required package wiring, which will break evaluation; update the snippet to
document and set services.happier-server.package (e.g. point it at a package
from your pkgs like pkgs.happier-server or show using
builtins.callPackage/overlay to provide the happier-server derivation) and
mention that environmentFile must contain HANDY_MASTER_SECRET (or demonstrate
using agenix/sops-nix) so users can copy the example and run it without
evaluation errors.

In `@modules/nixos/happier-server.nix`:
- Around line 179-184: The hardcoded Environment array sets DATABASE_URL to
localhost regardless of database.createLocally; update the logic that builds the
Environment for both ExecStart = "${cfg.package}/bin/happier-server-migrate" and
the runtime service (same pattern around ExecStart/services at lines referenced)
to conditionally construct DATABASE_URL: if cfg.database.createLocally is true,
keep the postgresql://${cfg.database.user}@localhost/${cfg.database.name} form,
otherwise do not inject a localhost URL and instead leave DATABASE_URL unset so
external wiring (env/secrets) can supply it (or use cfg.database.externalUrl
when provided); adjust the code that emits Environment to check
cfg.database.createLocally and/or cfg.database.externalUrl and only include the
DATABASE_URL entry when appropriate.
- Around line 91-94: The module currently allows rootCredentialsFile to default
to null while minio-bucket-init (and the full-mode setup) requires
MINIO_ROOT_USER/PASSWORD, so add an assertion in the nix module to fail fast
when an incompatible config is selected: check that rootCredentialsFile != null
whenever the option that enables minio-bucket-init or the "full" deployment mode
is set (referencing the rootCredentialsFile option and the
minio-bucket-init/full-mode enabling option names found later in the file), and
emit a clear error message instructing the user to provide a credentials file or
disable minio-bucket-init/full-mode; apply the same assertion logic for the
other related option block referenced around lines 135-148.
- Around line 111-115: The PostgreSQL auth block (authentication = lib.mkForce
'') is too permissive — change the pg_hba entries so trust is not granted to all
local users; replace "local all all trust" and the host-wide trust entries with
rules scoped to the configured database and user (e.g. "local <db_name>
<db_user> trust" or better "local <db_name> <db_user> scram-sha-256/md5") and
restrict host entries to the specific DB user and client addresses instead of
"host all all 127.0.0.1/32 trust" and "host all all ::1/128 trust"; update the
authentication block accordingly so only the intended DB/user can use trust (or
use password auth) rather than allowing all.
- Around line 214-217: The script sets DB="%S/..." which leaves %S literal so
the WAL setup may be skipped; update DB construction to use the runtime state
directory environment variable (e.g., $XDG_STATE_HOME or a provided $STATE_DIR)
and fall back to a sensible default if unset, then use that expanded variable in
the sqlite3 invocation (references: DB variable and the sqlite3 "$DB" "PRAGMA
journal_mode=WAL; PRAGMA busy_timeout=5000;"). Ensure the env var is referenced
with a $ prefix and that the resulting path is quoted, and keep the existing -f
check and sqlite3 call but pointing at the correctly expanded path.

In `@packages/prisma-engines-prebuilt.nix`:
- Around line 20-27: The current parsing of yarn.lock is brittle: ensure
engineVersionLines and prismaVersionLines are non-empty and that builtins.match
found a capture group before calling builtins.head and builtins.elemAt; if any
of these checks fail, call builtins.error with a clear message. Specifically,
after computing engineVersionLines and prismaVersionLines, assert
builtins.length engineVersionLines > 0 and builtins.length prismaVersionLines >
0, run builtins.match on the head lines and verify the match result is not null
and has the expected capture (use elemAt ... 1 to extract the first capture
group, not 0), and if any check fails, fail fast with an explicit error
mentioning the problematic pattern and the offending line.

In `@README.md`:
- Around line 177-195: The fenced code block showing the repo tree in README.md
is missing a language specifier; update the opening fence from ``` to ```text
for the repository-structure block so markdownlint MD040 passes (i.e., change
the block that starts with the tree listing to begin with ```text and close with
```). Ensure only that code fence is modified and the tree contents (the lines
with flake.nix, packages.nix, modules/, examples/, .github/, etc.) remain
unchanged.

---

Nitpick comments:
In `@flake.nix`:
- Around line 116-138: Add a preflight guard before the replacement loop to
ensure the script has exactly six hashes and the expected NEW_HASHES keys so
replacements cannot drift: check the length of CURRENT_HASHES (using
${`#CURRENT_HASHES`[@]}) equals 6 and verify each required NEW_HASHES key exists
(aarch64-linux_qe, aarch64-linux_se, aarch64-darwin_qe, aarch64-darwin_se,
x86_64-linux_qe, x86_64-linux_se); if the check fails, print a clear error
mentioning NIX_FILE and abort (exit 1) instead of proceeding to the loop that
uses CURRENT_HASHES, ORDERED_NEW and the for loop over ${!CURRENT_HASHES[@]}.

In `@packages.nix`:
- Around line 319-320: The packaging step currently masks TypeScript typecheck
failures by appending '|| echo "WARN: tsc --noEmit had errors..."' to the tsc
--noEmit invocation; remove the unconditional swallow and change the line so
that tsc --noEmit fails the build by default, but allow an explicit opt-out via
an environment flag (e.g., SKIP_SERVER_TYPECHECK=true) which, when set, runs tsc
--noEmit and prints a clear warning if it fails; specifically replace the
current command string containing "node ../../node_modules/typescript/bin/tsc
--noEmit || echo ..." so that failures propagate unless SKIP_SERVER_TYPECHECK is
set and handled with an if/else that logs the warning.
- Around line 164-167: The inline Node invocation that imports
'./apps/cli/scripts/buildSharedDeps.mjs' and calls syncBundledWorkspaceDist
should explicitly run Node in ESM mode; update the node command that currently
runs the eval block invoking syncBundledWorkspaceDist to include the flag
--input-type=module so the import('./...buildSharedDeps.mjs') is evaluated as
ESM (refer to the node -e invocation and the syncBundledWorkspaceDist
import/call).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13f3dafc-33fb-4b5d-98e8-3b7772a45901

📥 Commits

Reviewing files that changed from the base of the PR and between 1456d18 and dcf0f17.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • .github/workflows/nix-build.yml
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • checks.nix
  • devshell.nix
  • examples/happier-server-full.nix
  • examples/happier-server-light.nix
  • examples/happier-server-tailscale.nix
  • flake.nix
  • modules/nixos/happier-server.nix
  • packages.nix
  • packages/prisma-engines-prebuilt.nix
  • statix.toml

timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/nix-build.yml

Repository: happier-dev/nix-happier

Length of output: 2212


Pin workflow actions to immutable commit SHAs instead of mutable refs.

Using version tags (@v4) or branch refs (@main) allows upstream changes to alter CI behavior without review. Pin each uses: to a commit SHA for reproducibility and security.

This workflow uses:

  • actions/checkout@v4 (lines 20, 37, 51) — version tag is mutable
  • DeterminateSystems/nix-installer-action@main (lines 23, 40, 54) — branch ref is explicitly mutable
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/nix-build.yml at line 20, Update mutable action refs to
immutable commit SHAs: replace uses: actions/checkout@v4 with uses:
actions/checkout@<commit-sha> and replace
DeterminateSystems/nix-installer-action@main with
DeterminateSystems/nix-installer-action@<commit-sha>; do this for every
occurrence of those two action refs in the workflow file. Locate the exact
commit SHAs from each action's upstream GitHub repo (releases/tags or the commit
you want to pin), then substitute the tag/branch with that SHA to ensure
reproducible CI.

Comment on lines +11 to +20
```
flake.nix # Flake entrypoint (inputs, systems, imports)
packages.nix # happier-server + happier-cli derivations
checks.nix # deadnix, statix, NixOS VM integration test
devshell.nix # Dev shell (fmt, update commands)
modules/nixos/happier-server.nix # NixOS service module
packages/prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries
examples/happier-server-tailscale.nix # Recommended production setup (Tailscale + Caddy)
examples/happier-server-light.nix # Minimal config (used by CI VM test)
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language to the fenced code block.

The repo layout block should specify a language (e.g., text) to satisfy markdownlint MD040.

Proposed fix
-```
+```text
 flake.nix                        # Flake entrypoint (inputs, systems, imports)
 packages.nix                     # happier-server + happier-cli derivations
 checks.nix                       # deadnix, statix, NixOS VM integration test
 devshell.nix                     # Dev shell (fmt, update commands)
 modules/nixos/happier-server.nix # NixOS service module
 packages/prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries
 examples/happier-server-tailscale.nix # Recommended production setup (Tailscale + Caddy)
 examples/happier-server-light.nix    # Minimal config (used by CI VM test)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 11-11: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 11 - 20, The fenced code block in AGENTS.md (the repo
layout block listing flake.nix, packages.nix, etc.) is missing a language tag;
update the opening fence from ``` to ```text so the block becomes a ```text
fenced code block to satisfy markdownlint MD040.

Comment on lines +50 to +52
# Verify server responds (any HTTP response = service is up)
server.succeed("curl -sf http://localhost:3005/ || curl -sf -o /dev/null -w '%{http_code}' http://localhost:3005/ | grep -qE '^[0-9]'")
'';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Health-check can pass on transport failures (000).

The fallback command accepts any digit string, so non-HTTP failures can still pass the check.

Safer HTTP response check
-            server.succeed("curl -sf http://localhost:3005/ || curl -sf -o /dev/null -w '%{http_code}' http://localhost:3005/ | grep -qE '^[0-9]'")
+            server.succeed("curl -s -o /dev/null -w '%{http_code}' http://localhost:3005/ | grep -qE '^[1-5][0-9]{2}$'")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@checks.nix` around lines 50 - 52, The health check currently treats any digit
string (including curl's "000" on transport failure) as success; update the
server.succeed fallback so the curl -w '%{http_code}' output is validated
against a real HTTP status range (e.g. match three-digit codes 100–599) instead
of any digit string. Locate the server.succeed invocation that runs the two-curl
command (the line calling server.succeed with the fallback curl -w
'%{http_code}') and replace the grep pattern to only accept valid HTTP status
codes (for example, use a regex that matches ^[1-5][0-9]{2}$ or ^[1-5][0-9]{2}$
to exclude "000"). Ensure the primary -sf curl remains to succeed on normal
responses and the fallback only treats real HTTP codes as success.

Comment on lines +24 to +32
services.happier-server = {
enable = true;
port = 3005;
mode = "full";
# Required — must contain HANDY_MASTER_SECRET at minimum.
# Use agenix, sops-nix, or a plain file with restricted permissions.
environmentFile = config.age.secrets.happier-env.path;
minio.rootCredentialsFile = config.age.secrets.minio-credentials.path;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

services.happier-server.package is required but missing in this example.

modules/nixos/happier-server.nix defines package without a default, so this config is incomplete unless callers add it elsewhere. Please set it here or clearly document that it must be provided externally (as done in the light example).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/happier-server-full.nix` around lines 24 - 32, The example is
missing the required services.happier-server.package attribute
(modules/nixos/happier-server.nix defines package with no default); update the
example to include a package value for services.happier-server (or add a comment
making it explicit that callers must supply services.happier-server.package),
e.g. reference the appropriate derivation from your pkgs set or the same package
used in the light example so the module receives a concrete package value.

Comment on lines +19 to +26
services.happier-server = {
enable = true;
mode = "light";
port = 3005;
# Required — must contain HANDY_MASTER_SECRET at minimum.
# Use agenix, sops-nix, or a plain file with restricted permissions.
environmentFile = "/run/secrets/happier-env";
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add package wiring guidance so this example is runnable.

This example enables services.happier-server but does not show how to set the required services.happier-server.package, which can cause evaluation failure when copied directly.

Suggested docs tweak in this example
   services.happier-server = {
     enable = true;
+    # Also set `services.happier-server.package` in your host config, e.g.
+    # inputs.self.packages.${pkgs.system}.happier-server (flake-based setups).
     mode = "light";
     port = 3005;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
services.happier-server = {
enable = true;
mode = "light";
port = 3005;
# Required — must contain HANDY_MASTER_SECRET at minimum.
# Use agenix, sops-nix, or a plain file with restricted permissions.
environmentFile = "/run/secrets/happier-env";
};
services.happier-server = {
enable = true;
# Also set `services.happier-server.package` in your host config, e.g.
# inputs.self.packages.${pkgs.system}.happier-server (flake-based setups).
mode = "light";
port = 3005;
# Required — must contain HANDY_MASTER_SECRET at minimum.
# Use agenix, sops-nix, or a plain file with restricted permissions.
environmentFile = "/run/secrets/happier-env";
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/happier-server-tailscale.nix` around lines 19 - 26, The example
enables services.happier-server but omits the required package wiring, which
will break evaluation; update the snippet to document and set
services.happier-server.package (e.g. point it at a package from your pkgs like
pkgs.happier-server or show using builtins.callPackage/overlay to provide the
happier-server derivation) and mention that environmentFile must contain
HANDY_MASTER_SECRET (or demonstrate using agenix/sops-nix) so users can copy the
example and run it without evaluation errors.

Comment on lines +111 to +115
authentication = lib.mkForce ''
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
'';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PostgreSQL auth is overly broad (trust for all local users).

This weakens local isolation beyond what the service needs.

Scope trust to the configured DB/user at minimum
       authentication = lib.mkForce ''
-        local all all trust
-        host all all 127.0.0.1/32 trust
-        host all all ::1/128 trust
+        local ${cfg.database.name} ${cfg.database.user} trust
+        host ${cfg.database.name} ${cfg.database.user} 127.0.0.1/32 trust
+        host ${cfg.database.name} ${cfg.database.user} ::1/128 trust
       '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/nixos/happier-server.nix` around lines 111 - 115, The PostgreSQL auth
block (authentication = lib.mkForce '') is too permissive — change the pg_hba
entries so trust is not granted to all local users; replace "local all all
trust" and the host-wide trust entries with rules scoped to the configured
database and user (e.g. "local <db_name> <db_user> trust" or better "local
<db_name> <db_user> scram-sha-256/md5") and restrict host entries to the
specific DB user and client addresses instead of "host all all 127.0.0.1/32
trust" and "host all all ::1/128 trust"; update the authentication block
accordingly so only the intended DB/user can use trust (or use password auth)
rather than allowing all.

Comment on lines +179 to +184
ExecStart = "${cfg.package}/bin/happier-server-migrate";
Environment = [
"DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}"
"NODE_ENV=production"
"HOME=%S/happier-server"
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

database.createLocally = false is ignored by hardcoded local DATABASE_URL.

Both migration and runtime force localhost Postgres in full mode, preventing external DB wiring via env/secrets.

Respect `createLocally` when setting `DATABASE_URL`
         if isFullMode then
           {
             ExecStart = "${cfg.package}/bin/happier-server-migrate";
-            Environment = [
-              "DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}"
-              "NODE_ENV=production"
-              "HOME=%S/happier-server"
-            ];
+            Environment =
+              [
+                "NODE_ENV=production"
+                "HOME=%S/happier-server"
+              ]
+              ++ lib.optional cfg.database.createLocally
+                "DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}";
           }
@@
         Environment = [
           "NODE_ENV=production"
           "PORT=${toString cfg.port}"
           "HOME=%S/happier-server"
         ]
         ++ lib.optionals isFullMode (
-          [
-            "DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}"
-          ]
+          lib.optional cfg.database.createLocally
+            "DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}"
           ++ lib.optional cfg.redis.createLocally "REDIS_URL=redis://localhost:6379"

Also applies to: 272-273

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/nixos/happier-server.nix` around lines 179 - 184, The hardcoded
Environment array sets DATABASE_URL to localhost regardless of
database.createLocally; update the logic that builds the Environment for both
ExecStart = "${cfg.package}/bin/happier-server-migrate" and the runtime service
(same pattern around ExecStart/services at lines referenced) to conditionally
construct DATABASE_URL: if cfg.database.createLocally is true, keep the
postgresql://${cfg.database.user}@localhost/${cfg.database.name} form, otherwise
do not inject a localhost URL and instead leave DATABASE_URL unset so external
wiring (env/secrets) can supply it (or use cfg.database.externalUrl when
provided); adjust the code that emits Environment to check
cfg.database.createLocally and/or cfg.database.externalUrl and only include the
DATABASE_URL entry when appropriate.

Comment on lines +214 to +217
DB="%S/happier-server/.happy/server-light/happier-server-light.sqlite"
if [ -f "$DB" ]; then
sqlite3 "$DB" "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SQLite WAL path uses %S inside a shell script, so it won’t resolve.

In this script context, %S is treated literally, so WAL setup can be skipped unintentionally.

Use the runtime state directory env var
-        DB="%S/happier-server/.happy/server-light/happier-server-light.sqlite"
+        DB="$STATE_DIRECTORY/.happy/server-light/happier-server-light.sqlite"
         if [ -f "$DB" ]; then
           sqlite3 "$DB" "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
         fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DB="%S/happier-server/.happy/server-light/happier-server-light.sqlite"
if [ -f "$DB" ]; then
sqlite3 "$DB" "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
fi
DB="$STATE_DIRECTORY/.happy/server-light/happier-server-light.sqlite"
if [ -f "$DB" ]; then
sqlite3 "$DB" "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/nixos/happier-server.nix` around lines 214 - 217, The script sets
DB="%S/..." which leaves %S literal so the WAL setup may be skipped; update DB
construction to use the runtime state directory environment variable (e.g.,
$XDG_STATE_HOME or a provided $STATE_DIR) and fall back to a sensible default if
unset, then use that expanded variable in the sqlite3 invocation (references: DB
variable and the sqlite3 "$DB" "PRAGMA journal_mode=WAL; PRAGMA
busy_timeout=5000;"). Ensure the env var is referenced with a $ prefix and that
the resulting path is quoted, and keep the existing -f check and sqlite3 call
but pointing at the correctly expanded path.

Comment on lines +20 to +27
engineVersionLines = builtins.filter (
l: builtins.match ".*@prisma/engines-version.*" l != null
) lines;
engineHash = builtins.elemAt (builtins.match ".*\\.([a-f0-9]+).*" (builtins.head engineVersionLines)) 0;

prismaVersionLines = builtins.filter (l: builtins.match "\"@prisma/engines@.*" l != null) lines;
version = builtins.elemAt (builtins.match "\"@prisma/engines@([^\"]+)\".*" (builtins.head prismaVersionLines)) 0;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail explicitly when yarn.lock parsing does not match expected Prisma patterns.

This currently relies on builtins.head + permissive regexes; lockfile shape drift can produce hard-to-debug eval failures or wrong commit extraction.

Robust parsing guard
-  engineVersionLines = builtins.filter (
-    l: builtins.match ".*@prisma/engines-version.*" l != null
-  ) lines;
-  engineHash = builtins.elemAt (builtins.match ".*\\.([a-f0-9]+).*" (builtins.head engineVersionLines)) 0;
+  engineVersionLines = builtins.filter (l: builtins.match ".*@prisma/engines-version.*" l != null) lines;
+  engineHashMatch =
+    if engineVersionLines == [ ] then null
+    else builtins.match ".*\\.([a-f0-9]{40}).*" (builtins.head engineVersionLines);
+  engineHash =
+    if engineHashMatch == null then
+      throw "prisma-engines-prebuilt: unable to parse 40-char Prisma engine commit hash from yarn.lock"
+    else
+      builtins.elemAt engineHashMatch 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
engineVersionLines = builtins.filter (
l: builtins.match ".*@prisma/engines-version.*" l != null
) lines;
engineHash = builtins.elemAt (builtins.match ".*\\.([a-f0-9]+).*" (builtins.head engineVersionLines)) 0;
prismaVersionLines = builtins.filter (l: builtins.match "\"@prisma/engines@.*" l != null) lines;
version = builtins.elemAt (builtins.match "\"@prisma/engines@([^\"]+)\".*" (builtins.head prismaVersionLines)) 0;
engineVersionLines = builtins.filter (l: builtins.match ".*@prisma/engines-version.*" l != null) lines;
engineHashMatch =
if engineVersionLines == [ ] then null
else builtins.match ".*\\.([a-f0-9]{40}).*" (builtins.head engineVersionLines);
engineHash =
if engineHashMatch == null then
throw "prisma-engines-prebuilt: unable to parse 40-char Prisma engine commit hash from yarn.lock"
else
builtins.elemAt engineHashMatch 0;
prismaVersionLines = builtins.filter (l: builtins.match "\"@prisma/engines@.*" l != null) lines;
version = builtins.elemAt (builtins.match "\"@prisma/engines@([^\"]+)\".*" (builtins.head prismaVersionLines)) 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/prisma-engines-prebuilt.nix` around lines 20 - 27, The current
parsing of yarn.lock is brittle: ensure engineVersionLines and
prismaVersionLines are non-empty and that builtins.match found a capture group
before calling builtins.head and builtins.elemAt; if any of these checks fail,
call builtins.error with a clear message. Specifically, after computing
engineVersionLines and prismaVersionLines, assert builtins.length
engineVersionLines > 0 and builtins.length prismaVersionLines > 0, run
builtins.match on the head lines and verify the match result is not null and has
the expected capture (use elemAt ... 1 to extract the first capture group, not
0), and if any check fails, fail fast with an explicit error mentioning the
problematic pattern and the offending line.

Comment on lines +177 to +195
```
.
├── flake.nix # Flake entrypoint
├── flake.lock
├── packages.nix # happier-server + happier-cli derivations
├── checks.nix # deadnix, statix, NixOS VM test
├── devshell.nix # Dev shell with commands
├── modules/
│ └── nixos/
│ └── happier-server.nix # NixOS module
├── packages/
│ └── prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries
├── examples/
│ ├── happier-server-tailscale.nix # Production setup with Tailscale + Caddy
│ └── happier-server-light.nix # Minimal config (used by CI)
└── .github/
└── workflows/
└── nix-build.yml # CI workflow
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Specify a language for the repo-structure code fence.

This block should declare a language (text) to pass markdownlint MD040.

Proposed fix
-```
+```text
 .
 ├── flake.nix                          # Flake entrypoint
 ├── flake.lock
 ├── packages.nix                       # happier-server + happier-cli derivations
 ├── checks.nix                         # deadnix, statix, NixOS VM test
 ├── devshell.nix                       # Dev shell with commands
 ├── modules/
 │   └── nixos/
 │       └── happier-server.nix         # NixOS module
 ├── packages/
 │   └── prisma-engines-prebuilt.nix    # Prebuilt Prisma engine binaries
 ├── examples/
 │   ├── happier-server-tailscale.nix   # Production setup with Tailscale + Caddy
 │   └── happier-server-light.nix       # Minimal config (used by CI)
 └── .github/
     └── workflows/
         └── nix-build.yml              # CI workflow
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 177-177: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 177 - 195, The fenced code block showing the repo
tree in README.md is missing a language specifier; update the opening fence from
totext for the repository-structure block so markdownlint MD040 passes
(i.e., change the block that starts with the tree listing to begin with text and close with ). Ensure only that code fence is modified and the tree
contents (the lines with flake.nix, packages.nix, modules/, examples/, .github/,
etc.) remain unchanged.


</details>

<!-- fingerprinting:phantom:poseidon:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant