Migration 2025_08_31_224356_additional_languages_af_ca_id.php runs
during `migrate`, before db:seed's LanguageSeeder has populated the
other 42 rows of the languages table, and does
Cache::forever('languages', Language::all()) - permanently caching
just the 3 languages (Afrikaans, Catalan, Indonesian) it sees at that
moment. StaticServiceProvider checks this cache key before ever
querying the DB again, so every fresh install's language dropdown
(e.g. the "Welcome to Invoice Ninja" company setup wizard) was stuck
showing only those 3 entries, in their own native-language spelling
(ctrans() translates each name using the active locale).
Adds an unconditional `invoiceninja-manage cache:forget languages` to
invoiceninja-data-setup.service, and a regression test asserting
/api/v1/statics returns more than 40 languages. Verified directly:
before the fix, a fresh account's statics call returned exactly 3
languages; after, it returns all 45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this repo is
A Nix flake that packages Invoice Ninja (a PHP/Laravel app with a
separate React frontend) and provides a NixOS module (services.invoiceninja) to deploy it as a full service:
PHP-FPM pool, nginx or Caddy reverse proxy, MariaDB, Redis, a queue-worker systemd unit, and a one-shot
migration/setup unit.
There is no application source code here — this is packaging/deployment code only. All behavior changes come from editing Nix expressions.
Commands
This project uses just (see justfile) and Nix directly.
just build-vm [proxy]— cleans previous artifacts and builds the test VM (nixosConfigurations.test-<proxy>) vianom build.proxyisnginx(default) orcaddy.just boot-vm— boots the built VM (result/bin/run-nixos-vm) in the current terminal. Serves Invoice Ninja onlocalhost:8080(seeQEMU_NET_OPTSin thejustfile) with root logintoor.just clean— removesresult/nixos.qcow2and kills any runningqemuprocess.just test— runs the automated test suite (nix flake check -L).nix build .#invoiceninja— build just the Invoice Ninja package.nix build .#checks.x86_64-linux.invoiceninja -L— run only the automated VM test, with build/test log output.nix flake check— evaluate the flake (module options, overlay, package derivation) and run the automated test.
tests/
common.nix— the sharedservices.invoiceninjaconfig used by both the interactive VM and the automated test: imports the module and points at the checked-in test fixtures (invoice_ninja_test_password,test.env). It's a function taking{ proxyServer ? "nginx" }so callers can pick which proxy backend to test.default.nix— the interactive VM config (nixosConfigurations.test-nginx/test-caddyinflake.nix), used for manual, click-around verification viajust build-vm [proxy]/just boot-vm. It's also a function taking{ proxyServer ? "nginx" }, forwarded straight through tocommon.nix, soflake.nixcan instantiate onenixosConfigurationsentry per proxy backend. Adds the qemu-guest profile, a root login, andnixpkgs.config.allowUnfree = true(required to buildpkgs.chromium). It also sets a dummyfileSystems."/"and disablesboot.loader.grubpurely to satisfy NixOS's built-in assertions whennix flake checkevaluatesconfig.system.build.topleveldirectly — the qemu-vm module overrides both with a tmpfs root and direct kernel boot when actually buildingconfig.system.build.vm(whatjust build-vmuses), so these values are otherwise unused.nixos-test.nix— the automated test (pkgs.testers.nixosTest, exposed aschecks.<system>.invoiceninjainflake.nix). Builds its ownpkgs(withallowUnfreebaked in —nixosTestforbids settingnixpkgs.configon a node oncepkgsis externally supplied) and boots two nodes fromcommon.nix— one withproxyServer = "nginx", one withproxyServer = "caddy"— so both proxy backends get exercised. The shared assertion suite (factored into acheck_invoiceninja(machine, proxy_unit)function in the test script) runs against each node: waits for mysql/redis/phpfpm/queue-worker/proxy to be active, checks theinvoiceninja-data-setupmarker files (.initial-migration,.db-seeded,.admin-created) to confirm the one-time setup actually completed (not just that the oneshot unit exited), curls/for the app shell, exercises theinvoiceninja-managewrapper, and logs in via/api/v1/loginusing the test admin credentials as an end-to-end check that migrations/seeding/account creation all worked. This is the only automated test in the repo — there is no separate unit-test suite.
Architecture
package.nix
Builds Invoice Ninja in two stages:
ui— the separateinvoiceninja/uiReact frontend repo, built withbuildNpmPackage. Patches.env.exampleto disable test mode and swaps invite.config.ts.reactbefore building.- Main package — built with
php82.buildComposerProjectfrom theinvoiceninja/invoiceninjaPHP repo. Theuibuild'sdist/output is copied intopublic/before the PHP build (preConfigure), andpublic/index.htmlis copied toresources/views/react/index.blade.phpso Laravel serves the React shell.
Both source fetches (fetchFromGitHub) run their lockfiles through npm-lockfile-fix in postFetch to patch
missing integrity fields — required for fetchNpmDeps/buildNpmPackage to work with these upstream lockfiles.
Because Laravel needs writable directories that can't live in the read-only Nix store, postInstall renames
bootstrap → bootstrap-static and storage → storage-static inside the derivation, then symlinks
$out/.env, $out/storage, $out/public/storage, and $out/bootstrap to paths under the module's dataDir /
runtimeDir. This means the package output is only fully functional when paired with the NixOS module (or
something else that populates those runtime paths the same way) — building the package alone will produce
dangling symlinks.
When bumping the Invoice Ninja or UI version in package.nix, all four hashes (src.hash, uiSrc.hash,
ui's npmDepsHash, and the main package's vendorHash/npmDeps.hash) generally need to be recomputed —
expect Nix to report the correct hash on a failed build (or use nix-prefetch/lib.fakeHash workflow).
nixos-module/
default.nix— the flake'snixosModulesoutput. Exposes theinvoiceninjamodule and a smalloverlayNixpkgsForThisInstancemodule that applies the flake's overlay (registerspkgs.invoiceninja) to whatever nixpkgs instance imports it.invoiceninja.nix— the actualservices.invoiceninjamodule. Key structural points:cfg.settingsis a submodule with afreeformType(pkgs.formats.keyValue), so any Laravel.envkey can be set even if not explicitly declared as an option; explicitly declared options (APP_ENV, MAIL_MAILER, SESSION_DRIVER, etc.) exist mainly to add type-checking/enums and sensible computed defaults (e.g. SESSION_DRIVER/QUEUE_CONNECTION/CACHE_DRIVER default to redis → database → file depending on what's enabled).- Secrets are deliberately kept out of
cfg.settings/the Nix store.secretFile(required) andadminAccount.passwordFile(required) are consumed via systemdLoadCredentialin theinvoiceninja-data-setupservice, not written into the generated.envderivation. The final.envis assembled at activation time by concatenating the Nix-store-rendered non-secret settings with the secret file contents directly on disk indataDir. invoiceninja-data-setup.service(oneshot, runs on every activation) does all first-run and idempotent setup: clears bootstrap/opcode cache, rebuilds.env, rsyncs static storage intodataDir, symlinks the runtimeapp.php, and guards one-time actions (migrate,db:seed, admin account creation) behind marker files (.initial-migration,.db-seeded,.admin-created) indataDir.phpfpm-invoiceninja.serviceandinvoiceninja-queue-worker.serviceboth depend on it. It also unconditionally runsinvoiceninja-manage cache:forget languageson every activation, working around an upstream bug: migration2025_08_31_224356_additional_languages_af_ca_id.phpruns duringmigrate(beforedb:seed'sLanguageSeederhas populated the other 42 rows) and doesCache::forever('languages', Language::all()), permanently caching just the 3 languages it sees at that moment — without this, the language dropdown in the app stays stuck at 3 entries forever, sinceStaticServiceProviderchecks that cache key before ever querying the DB again.invoiceninja-scheduler.timer/.servicerunartisan schedule:runevery minute — this is what drives Invoice Ninja's own internal cron-scheduled jobs (recurring invoices, scheduled reports, etc.); without it the app's dashboard reports "cron jobs are not enabled". Laravel's scheduler is just a dispatcher, so this one timer is sufficient regardless of how many scheduled tasks Invoice Ninja itself defines.invoiceninja-manageis a generated wrapper script (invoiceninja-manage <artisan-args>) thatcds into the package output and runsphp artisanas the configured service user (viasudoif invoked as another user). It's added toenvironment.systemPackages, so it's the normal way to run artisan commands (migrate,queue:work,ninja:create-account, etc.) by hand on a deployed system.- Both
nginxandcaddyproxy backends are supported (proxy.server), each independently duplicating the PHP-FPM passthrough config — if changing one, check whether the other needs the same change. Neither sets CORS headers itself: Laravel's own CORS middleware already sends a complete set on every response, and an earlier version of this module additionally set a second, static set ofAccess-Control-*headers at the proxy level, which browsers reject as conflicting duplicates (server-to-server/curl checks don't notice, since only browsers enforce CORS header validation). - Caddy matches vhosts on the exact
Hostheader, unlike nginx (where a single vhost acts as the default regardless ofHost). Whenhostname == "localhost", the caddy vhost addsserverAliasesfor127.0.0.1:80/[::1]:80too — otherwise visiting the VM via the IP literal instead oflocalhostmatches no vhost and Caddy silently returns an empty200 OK(a blank page with no error anywhere). - Chromium is required at runtime for PDF generation (
PDF_GENERATOR=snappdf,SNAPPDF_CHROMIUM_PATH) and is added to the PHP-FPM and data-setup services'path.
flake.nix
Standard flake shape: overlays.default registers the package, packages.<system>.invoiceninja exposes it,
nixosModules re-exports nixos-module/default.nix, and nixosConfigurations.test-nginx/test-caddy each build
the VM defined in tests/default.nix with the corresponding proxyServer. Only x86_64-linux is currently
supported.
Conventions
- Nix files: 2-space indentation (see
.editorconfig); Unix line endings; final newline required. - Prefer
lib.mkDefault/lib.mkIf/lib.mkMergefor module config so downstream users can override cleanly — this is used pervasively ininvoiceninja.nixand should be followed for new options/config.