No description
  • PHP 56.1%
  • Vue 22.2%
  • TypeScript 14.8%
  • Nix 4%
  • Shell 1.3%
  • Other 1.4%
Find a file
Yisroel Baum e2545c32ac
All checks were successful
Update home server / update (push) Successful in 8s
Merge branch 'npm-warnings'
2026-11-01 23:00:18 +02:00
.forgejo/workflows fix nix workflow container compatibility 2026-10-13 23:06:29 +03:00
ai document nix browser testing workflow 2026-10-31 22:54:16 +02:00
backend use child layout enum 2026-09-23 23:33:06 +03:00
frontend/rabbi_gerzi stabilize upgraded pdf browser lifecycle 2026-11-01 09:18:59 +02:00
nix stabilize upgraded pdf browser lifecycle 2026-11-01 09:18:59 +02:00
scripts add testing and seeding recipes 2026-10-31 20:37:32 +02:00
.envrc add onefetch 2026-06-25 22:07:33 +03:00
.gitignore add isolated worktree stacks 2026-09-21 23:04:08 +03:00
AGENTS.md document worktree workflow 2026-09-21 23:07:00 +03:00
flake.lock update nixpkgs 2026-10-22 20:15:58 +03:00
flake.nix use current nixfmt package 2026-11-01 08:32:45 +02:00
justfile run browser matrix through just 2026-10-31 22:54:17 +02:00
notes.md update notes with todo list 2026-06-25 22:52:00 +03:00
pilzno_brand_guide.pdf add pilzno brand guide pdf 2026-06-06 21:24:14 +03:00
process-compose.yml align local frontend hostname 2026-09-23 23:13:05 +03:00
README.md document dependency maintenance exception 2026-11-01 08:35:18 +02:00
RECOVERY.md document borg backup recovery 2026-08-17 20:52:30 +03:00
social-preview-todo.md document social preview 2026-07-03 15:10:46 +03:00

Rabbi Gerzi

Rabbi Gerzi is the public website and Torah media application for Rabbi Yehoshua Gerzi. The site presents Rabbi Gerzi's work, projects, and Torah media, with an authenticated admin area for managing media sets, nested elements, rich text, ordered YouTube, Vimeo, and audio media, icons, and PDF uploads.

The application is split into:

  • backend/: Laravel 13, PHP 8.3, PostgreSQL, PHPUnit.
  • frontend/rabbi_gerzi/: Vue 3, Vite, TypeScript, Pinia, Cypress.
  • nix/: Nix packages, checks, upload limits, and the NixOS service module.

Production assumptions:

  • Website: https://rabbigerzi.com
  • API: https://api.rabbigerzi.com

All local and deployment workflows are Nix-first. Run commands from nix develop, or use direnv with the checked-in .envrc.

How the Application Works

Content and Public Browsing

Torah media is organized into sets. Each set has a name, description, icon, and one root element. Elements form an ordered tree: the root can have child elements, and each child can have further children.

Visitors browse the available sets at /media. Opening a set loads its root element, where the visitor can move through parent, child, and sibling links. An element can present an icon, formatted rich text, an ordered list of YouTube videos, Vimeo videos, and uploaded audio, and short or long PDFs. On desktop the media occupies a left rail while the main content remains centered. On mobile it follows the main content. The PDF viewer supports page navigation, zoom, search, download, printing, and opening the source document in a new tab.

Admin Editing

Administrators sign in at /login. An authenticated session reveals the management controls on /media and protects all content-changing API calls.

From the media page, an administrator can:

  • Create a set with a name, description, and icon. This also creates its root element and opens that element in edit mode.
  • Edit an existing set's card details or replace its icon.
  • Delete a set together with its complete element tree and managed uploads.

The Edit Element action on a public element opens its admin editor. The editor supports:

  • Editing the title, description, and rich text. Rich text includes headings, inline formatting, alignment, lists, links, and tables.
  • Adding, editing, removing, and reordering YouTube and Vimeo URLs, and uploading or removing audio files up to 50 MiB.
  • Uploading or removing an icon, a short PDF, and a long PDF.
  • Adding child elements, moving direct children up or down, and opening a child directly in edit mode.
  • Removing a child together with all of its descendants and managed uploads.

Title, description, and rich text changes are written by the Save action. Media, icon, and PDF changes are applied immediately. The View Element action returns to the public presentation, and Logout ends the admin session. Development login credentials are listed under Local Development.

Required Secrets and Environment

Do not commit real secret values. Local examples can live in ignored .env files, and production secrets should live outside the Nix store.

Required Production Secrets

The NixOS module requires services.rabbi-gerzi.backend.environmentFile. That file must contain:

APP_KEY=base64:replace-with-laravel-app-key
RABBI_GERZI_INITIAL_ADMIN_EMAIL=admin@example.com
RABBI_GERZI_INITIAL_ADMIN_PASSWORD=replace-with-a-long-random-password
  • APP_KEY encrypts and signs Laravel application data.
  • RABBI_GERZI_INITIAL_ADMIN_EMAIL is the first admin login email.
  • RABBI_GERZI_INITIAL_ADMIN_PASSWORD is used only to create that admin if it does not already exist.

The module also requires paths to a Borg repository passphrase file and SSH private key through services.rabbi-gerzi.secretFiles. Keep both files outside the Nix store and readable only by root.

Generate an app key inside the dev shell:

cd backend
php artisan key:generate --show

Public Frontend Config

The frontend build needs the public API origin:

VITE_API_BASE_URL=https://api.rabbigerzi.com

For NixOS deployment this is handled by services.rabbi-gerzi.frontend.apiBaseUrl, which defaults to the backend public URL.

NixOS Runtime Config

The NixOS module supplies these non-secret runtime values:

Setting Production value
APP_ENV production
APP_DEBUG false
APP_URL https://api.rabbigerzi.com
CORS_ALLOWED_ORIGINS https://rabbigerzi.com by default
DB_CONNECTION pgsql
DB_HOST /run/postgresql
DB_DATABASE rabbi-gerzi by default
DB_USERNAME rabbi-gerzi by default
FILESYSTEM_DISK public
LARAVEL_STORAGE_PATH /var/lib/rabbi-gerzi/storage by default
SESSION_DRIVER database
SESSION_DOMAIN empty by default, meaning host-only cookies
SESSION_SAME_SITE none by default
SESSION_SECURE_COOKIE true by default
CACHE_STORE file
QUEUE_CONNECTION sync
MAIL_MAILER log

The module also manages PostgreSQL, php-fpm, nginx, writable storage/cache directories. PHP accepts audio files up to 50 MiB, while PHP request bodies and nginx accept 51 MiB to leave room for multipart form overhead.

Local Environment Files

Backend local configuration starts from:

cd backend
cp .env.example .env
php artisan key:generate

The checked-in example already targets the Nix dev PostgreSQL socket through PGHOST, and uses the postgres local database/user.

Frontend local configuration should point Vite at the local backend:

cd frontend/rabbi_gerzi
printf '%s\n' 'VITE_API_BASE_URL=http://localhost:8000' > .env.local

Production Backups

The NixOS module creates an encrypted Borg backup in:

ssh://mgjjruz9@mgjjruz9.repo.borgbase.com/./repo

Each archive contains the complete application state directory and a custom-format PostgreSQL dump. This covers database records, users, sessions, and every uploaded icon, PDF, and audio file. The Nix store, runtime secrets, and /var/cache/rabbi-gerzi are reproducible or recovered separately and are not archived.

The persistent timer runs daily at 05:15 in the server's local timezone and keeps 7 daily, 4 weekly, and 6 monthly archives. PHP-FPM drains active requests and remains stopped while the dump and archive are created, so database rows and uploaded files come from the same write-free window. The static frontend remains available during that window.

Run an immediate backup with:

sudo systemctl start borgbackup-job-rabbi-gerzi.service

See RECOVERY.md for archive verification and full restoration.

Local Development

Enter the Nix dev shell before running local development commands:

nix develop

If direnv is installed:

direnv allow

The dev shell provides PHP, Composer, Node, PostgreSQL, Cypress, process-compose, TypeScript tooling, formatters, linters, and Nix formatters. It also initializes a local PostgreSQL cluster in .postgres when needed.

Install dependencies:

cd backend
composer install

cd ../frontend/rabbi_gerzi
npm install --legacy-peer-deps

Create local environment files:

cd ../../backend
cp .env.example .env
php artisan key:generate

cd ../frontend/rabbi_gerzi
printf '%s\n' 'VITE_API_BASE_URL=http://localhost:8000' > .env.local

Start the local services from the repo root:

process-compose up

This starts:

  • PostgreSQL on the local Unix socket under .postgres.
  • Laravel at http://localhost:8000.
  • Vite at http://localhost:5173.

Open the frontend using localhost, and use localhost in the frontend's API URL too. These hostnames keep CORS and login cookies consistent. Linked worktrees use the same hostname with their assigned port offsets.

In another dev-shell terminal, prepare the database:

cd backend
php artisan migrate:fresh --seed

The seed admin login is:

admin@rabbigerzi.test
password123!@#

Testing

Run local tests from nix develop. To run the entire local test suite, keep the app stack running in one dev-shell terminal:

nix develop
process-compose up

Then run backend, frontend, and Cypress checks from another dev-shell terminal. process-compose provides the PostgreSQL database, Laravel backend, and Vite frontend needed by the full suite.

Backend tests live under backend/tests.

  • Unit tests exercise domain entities, use cases, middleware, controllers, and fake repositories without requiring PostgreSQL.
  • Feature tests exercise HTTP endpoints, Laravel routing, migrations, seed data, and real framework integration.
  • PHPUnit is configured to use an in-memory SQLite database for test runs.

Run backend tests:

cd backend
./vendor/bin/phpunit tests

Run backend style checks:

cd backend
phpcs app tests routes database

Frontend quality checks live under frontend/rabbi_gerzi.

  • npm run format checks and formats Vue and TypeScript source with oxfmt.
  • npm run lint runs oxlint and eslint.
  • npm run type-check runs vue-tsc against the Vite application.

Run frontend checks:

cd frontend/rabbi_gerzi
npm run format
npm run lint
npm run type-check

Cypress E2E tests live under frontend/rabbi_gerzi/cypress/e2e.

  • Specs drive the UI through the Vite frontend at http://localhost:5173.
  • The Laravel backend must be running at http://localhost:8000.
  • The Cypress config creates a seeded PostgreSQL template database before the run and resets the app database between specs with the db:reset task.
  • Seeded media assets are copied into backend/storage/app/public for each reset.

Run E2E tests after process-compose up is healthy:

cd frontend/rabbi_gerzi
npm run test:e2e

Open the Cypress runner:

cd frontend/rabbi_gerzi
npm run test:e2e:open

Safari rendering in Cypress

Cypress supports experimental WebKit testing. WebKit uses Safari's rendering engine, but it is not the macOS Safari application. Confirm visual changes in actual Safari as well.

From a provisioned feature worktree, start its isolated stack and run:

WORKTREE_ROOT="$(git rev-parse --show-toplevel)"
NIX_REMOTE=daemon direnv exec "$WORKTREE_ROOT" process-compose up -D
cd frontend/rabbi_gerzi
NIX_REMOTE=daemon direnv exec "$WORKTREE_ROOT" npm run test:e2e:open -- \
  --e2e --browser webkit

The x86_64 Linux devshell supplies Cypress, Chromium, Firefox, geckodriver, and both graphical and headless WebKit through Nix. No manual browser or system-library installation is needed. Cypress verification state stays in this worktree's .direnv directory. Opening a window requires a desktop display.

The shell disables Playwright's distribution-specific host-library check; Nix patches the browser libraries instead. This does not skip browser tests. Other platforms retain their existing shell setup.

Choose the homepage specs in the Cypress window. For headless testing, use npm run test:e2e -- --browser webkit. Repeat with --browser chromium and --browser firefox, sequentially because the tests reset the same database. Always run these commands through the worktree's direnv environment.

Verification

Start the worktree stack, then run the complete gate through its devshell:

WORKTREE_ROOT="$(git rev-parse --show-toplevel)"
NIX_REMOTE=daemon direnv exec "$WORKTREE_ROOT" process-compose up -D
NIX_REMOTE=daemon direnv exec "$WORKTREE_ROOT" just test-all
NIX_REMOTE=daemon direnv exec "$WORKTREE_ROOT" process-compose down

The gate checks PHP, frontend formatting/lint/types, Nix builds, and Cypress in Chromium, Firefox, and WebKit. Browser suites run sequentially because they reset the same test database; independent checks run in parallel. Use just fe-cypress-run --browser webkit for a focused browser run or just fe-cypress --e2e --browser webkit to open the interactive runner.

Deployment

Automatic server updates

Pushes to master run the Update home server Forgejo workflow on the linux runner using a pinned Debian-based Node container. A setup step installs Nix 2.28.7 from its official archive after verifying its SHA-256 checksum. The Debian base uses regular account files, avoiding the containerd error openat etc/group: path escapes from parent caused by absolute account-file symlinks in the Nix image. The workflow then clones yisroelbaum/home-server-config over SSH on port 2222 and runs:

nix flake update rabbi-gerzi

When flake.lock changes, the workflow commits only that file with the message update rabbi gerzi and pushes to the server repository's master. That push triggers the server repository's deployment workflow. The update resolves the latest revision of the configured input when it runs. An unchanged lock file finishes successfully without a commit or push.

One-time setup:

  1. Publish and bootstrap the deployment workflow in home-server-config before activating this handoff. Actions must be enabled in both repositories and the linux runner must be available to Rabbi Gerzi.

  2. Generate a dedicated deploy key outside the repository:

    ssh-keygen -t ed25519 -N '' -C rabbi-gerzi-ci \
      -f ~/.ssh/rabbi-gerzi-home-server
    
  3. Add the .pub file to the server repository's deploy keys with write access. Its branch protection must allow this key to push to master.

  4. Store the complete private key as HOME_SERVER_CONFIG_SSH_KEY in Rabbi Gerzi's Actions secrets. This is separate from the server's DEPLOY_SSH_KEY used for deployment.

  5. Publish this workflow on master. Confirm the Update home server run succeeds and, when the lock changes, that the downstream deployment starts.

The workflow pins Forgejo's trusted SSH host key for [git.yisroelbaum.com]:2222, with fingerprint SHA256:z9cNCTjRmKicJlqZyML7CQTw8bCzojFg0E4b/ei/+ME. Verify a replacement through a trusted connection before changing the pin. The job removes its temporary credentials and clone when it exits.

Failures stop the job. If another change reaches the server repository before the push, Git rejects the push; rerun the workflow to update a fresh clone. The workflow never force-pushes. Disable Update home server in Forgejo to pause this handoff.

When changing the container or Nix installation, run the Docker regression test in addition to nix flake check. It requires a running Docker daemon and uses the check's Python/PyYAML environment:

nix develop .#checks.x86_64-linux.home-server-update \
  --command python3 nix/checks/home-server-container-test.py \
  .forgejo/workflows/update-home-server.yml

The test starts the pinned image, checks the account files, runs installation and verification through separate docker exec calls, and updates a local fixture flake. It removes its container afterward and does not push to Forgejo.

NixOS module

Deploy through the flake's NixOS module. The module builds the frontend with VITE_API_BASE_URL=https://api.rabbigerzi.com, serves it from nginx, runs the Laravel backend with php-fpm, provisions PostgreSQL, creates writable storage and cache directories, runs migrations, and ensures the initial admin exists.

Example host configuration:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    rabbi-gerzi.url = "git+ssh://git@example.com/rabbi-gerzi.git";

    sops-nix = {
      url = "github:Mic92/sops-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      nixpkgs,
      rabbi-gerzi,
      sops-nix,
      ...
    }:
    {
      nixosConfigurations.rabbigerzi = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          rabbi-gerzi.nixosModules.default
          sops-nix.nixosModules.sops
          (
            { config, ... }:
            let
              appKey = config.sops.placeholder."app-key";
              initialAdminEmail =
                config.sops.placeholder."admin-email";
              initialAdminPassword =
                config.sops.placeholder."admin-password";
            in
            {
              sops = {
                defaultSopsFile = ./secrets/rabbi-gerzi.yaml;
                age.sshKeyPaths = [
                  "/etc/ssh/ssh_host_ed25519_key"
                ];

                secrets = {
                  "app-key" = { };
                  "admin-email" = { };
                  "admin-password" = { };
                  "borg-passphrase" = { };
                  "borg-private-key" = { };
                };

                templates."rabbi-gerzi.env".content = ''
                  APP_KEY=${appKey}
                  RABBI_GERZI_INITIAL_ADMIN_EMAIL=${initialAdminEmail}
                  RABBI_GERZI_INITIAL_ADMIN_PASSWORD=${initialAdminPassword}
                '';
              };

              services.rabbi-gerzi = {
                enable = true;

                secretFiles = {
                  borgPassphrase =
                    config.sops.secrets."borg-passphrase".path;
                  borgPrivateKey =
                    config.sops.secrets."borg-private-key".path;
                };

                frontend = {
                  hostName = "rabbigerzi.com";
                  redirectHostNames = [ "www.rabbigerzi.com" ];
                };

                backend = {
                  hostName = "api.rabbigerzi.com";
                  environmentFile =
                    config.sops.templates."rabbi-gerzi.env".path;
                };
              };
            }
          )
        ];
      };
    };
}

If the API origin must be set explicitly, use:

services.rabbi-gerzi.frontend.apiBaseUrl =
  "https://api.rabbigerzi.com";

Configure .sops.yaml with age recipients for each administrator and the server's SSH Ed25519 host key. Then create the encrypted secrets file:

sops secrets/rabbi-gerzi.yaml

Add these values in the editor opened by sops:

app-key: base64:replace-with-laravel-app-key
admin-email: admin@example.com
admin-password: replace-with-a-long-random-password
borg-passphrase: replace-with-the-repository-passphrase
borg-private-key: |
  -----BEGIN OPENSSH PRIVATE KEY-----
  replace-with-the-private-key
  -----END OPENSSH PRIVATE KEY-----

Commit only the encrypted file written by sops. At activation time, sops-nix decrypts these values and renders the runtime environment file outside the Nix store.

Then rebuild the host with the deployment method used by the server, for example:

sudo nixos-rebuild switch --flake .#rabbigerzi

During activation, rabbi-gerzi-setup runs:

php artisan migrate --force
php artisan rabbi-gerzi:ensure-initial-admin

The initial admin command is idempotent. If the configured admin email already exists, it leaves the user unchanged.

Production element-media migration

The element-media schema change must be released in two phases because production already contains values in elements.youtube_url. Take and verify a fresh backup before each phase. Keep the expansion and contract migrations in separate release revisions; do not deploy both for the first phase.

Phase 1 is the expansion release. It includes 2026_09_06_000000_create_element_media_table.php, the media domain model, and the compatibility code, but excludes the contract migration. Deploy it during a brief write outage:

sudo systemctl start borgbackup-job-rabbi-gerzi.service
sudo systemctl stop phpfpm-rabbi-gerzi.service
sudo nixos-rebuild switch --flake .#rabbigerzi
sudo systemctl stop phpfpm-rabbi-gerzi.service

The migration validates every nonblank legacy URL before creating the new table. Invalid URLs abort the migration and report their element IDs; correct those records deliberately and retry instead of silently discarding them. It then trims valid URLs, backfills one ordered YouTube media row per legacy URL, and keeps both representations synchronized.

Before restoring traffic, run this audit against the production database. It must return no rows:

SELECT e.id, e.youtube_url, media.source
FROM elements AS e
LEFT JOIN LATERAL (
  SELECT em.source
  FROM element_media AS em
  WHERE em.element_id = e.id
    AND em.media_type = 'youtube'
  ORDER BY em.sort_order, em.id
  LIMIT 1
) AS media ON TRUE
WHERE NULLIF(BTRIM(e.youtube_url), '') IS DISTINCT FROM media.source;

Then restore service and exercise a public element plus an authenticated edit:

sudo systemctl start phpfpm-rabbi-gerzi.service
sudo systemctl is-active phpfpm-rabbi-gerzi.service

Run phase 1 in production long enough to confirm logs, reads, and edits are healthy. Phase 2 is the contract release represented by the final code. It includes the media endpoints and UI plus 2026_09_06_000001_drop_youtube_url_from_elements_table.php. Take another verified backup, stop PHP-FPM, run the audit again, and deploy the contract revision. The migration repeats the comparison and refuses to drop the legacy column if any element differs. Start PHP-FPM and verify adding, editing, reordering, and deleting video media and uploading and playing audio.

If phase 2 must be rolled back, stop PHP-FPM first, roll back the contract migration, and activate the phase-1 application revision before restoring traffic. That migration recreates elements.youtube_url from the first ordered YouTube item and leaves all media rows intact. The phase-1 migration also refuses rollback when doing so would discard Vimeo, audio, or additional media rows.

Bluehost CDN activation

The NixOS module prepares responses for shared caching, but it does not place the public host names behind a CDN. A person with access to the Bluehost account must enable Bluehost's Cloudflare integration after deploying the origin configuration.

The origin sends these cache policies:

  • SPA HTML requires browser revalidation and requests a five-minute Cloudflare TTL when webpage caching is enabled.
  • Generated JavaScript and CSS are public and immutable for one year.
  • Stable frontend images, fonts, and PDFs are public for one week.
  • Uploaded files under /storage/ are public for 30 days.
  • API JSON, authenticated responses, and writes remain private and dynamic.

Before enabling the CDN, verify that the origin has valid public certificates for rabbigerzi.com, www.rabbigerzi.com, and api.rabbigerzi.com. Do not enable strict proxying while any host is serving a temporary self-signed certificate.

Ask the Bluehost account owner to complete these steps:

  1. Open Websites, select Manage Site, and open Performance.
  2. Enable Cloudflare.
  3. If Caching Control is available, enable it and select Assets & Web Pages - Extended. This keeps web pages for five minutes and assets for one week at the CDN edge.
  4. Confirm that Cloudflare covers rabbigerzi.com, www.rabbigerzi.com, and api.rabbigerzi.com. If the API subdomain is omitted, ask Bluehost support to proxy it before treating the rollout as complete.

If Caching Control is unavailable, Cloudflare still caches standard static file types such as JavaScript, CSS, images, fonts, and PDFs. HTML and JSON are not cached by default. The origin's Cloudflare-specific header controls the HTML lifetime only after webpage caching makes HTML eligible.

Bluehost documents this flow here: https://www.bluehost.com/help/article/cloudflare-enable-cdn. The domain registration and authoritative DNS can remain at Bluehost.

After activation and DNS propagation, request the same URL at least twice from the same client:

curl -sSI https://rabbigerzi.com/
curl -sSI https://rabbigerzi.com/assets/CURRENT_BUILD_FILE.js
curl -sSI https://api.rabbigerzi.com/storage/CURRENT_UPLOAD.png
curl -sSI https://api.rabbigerzi.com/api/sets

Replace the example asset and upload paths with URLs from the current site. A cached frontend or upload response must include cf-ray and eventually cf-cache-status: HIT. The API response must retain Cache-Control: no-cache, private and must never report HIT. Also confirm that https://www.rabbigerzi.com has valid TLS and redirects to the canonical host. Expect the / response to reach HIT only when webpage caching is enabled; asset-only Cloudflare configurations may report it as DYNAMIC.

No deployment purge is required for normal frontend releases. Generated asset names are content-hashed, and cached HTML expires after five minutes when webpage caching is enabled. When a stable-path image or PDF must update immediately, use Bluehost's Clear URL control. Use Clear All only when a targeted purge is insufficient.

Useful Nix Outputs

Build the frontend package:

nix build .#frontend

Build the backend package:

nix build .#backend

Evaluate all checks:

nix flake check

Just commands

The Nix devshell provides just. Run just to list recipes. With this checkout's stack running, use just test-all for the full parallel gate. Set JUST_JOBS=2 to reduce concurrency. Cypress runs as one job because it resets the application database. Failures retain per-job logs and make the gate exit unsuccessfully.

Use just fresh to rebuild and seed the local database. This deletes existing database data. Seeders are intended for a fresh database.

From a linked worktree root, run commands through its environment:

NIX_REMOTE=daemon direnv exec "$PWD" just test-all
NIX_REMOTE=daemon direnv exec "$PWD" just fresh

Use just test --filter Example for selected backend tests and just fe-cypress-run --spec cypress/e2e/login.cy.ts for selected E2E tests. Frontend fix recipes are separate from the gate's check-only recipes.

Dependency maintenance

After merging dependency changes, refresh the installed packages from the repository root with:

NIX_REMOTE=daemon direnv exec "$PWD" just fe-npm ci --legacy-peer-deps

Install-script approvals in the frontend package manifest are version-pinned. Review and renew them when upgrading Cypress, esbuild, or Playwright WebKit. The Nix environment suppresses browser downloads and supplies the binaries.

Audit exception (2026-11-01): GHSA-g3hv-75wg-m249 affects minimatch below 11.0.1. ESLint (directly and through @eslint/config-array) and @vue/eslint-config-typescript -> typescript-eslint -> @typescript-eslint/typescript-estree still require minimatch 10. This is development-only tooling processing repository file patterns; it is not shipped in the site. Ten moderate audit entries inherit this single issue. Remove this exception when supported upstream releases adopt a patched minimatch version. Do not force an incompatible major or downgrade ESLint. Production dependencies must remain free of audit findings.