Rabbi_Gerzi/README.md

13 KiB

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, YouTube links, 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 embedded YouTube video, and short or long PDFs. 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, rich text, and YouTube URL. Rich text includes headings, inline formatting, alignment, lists, links, and tables.
  • 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, rich text, and YouTube changes are written by the Save action. Icon and PDF changes are uploaded or removed 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, and the 6 MB upload limit for nginx and PHP.

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://127.0.0.1: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 and PDF. 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://127.0.0.1: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://127.0.0.1:8000.
  • Vite at http://localhost:5173.

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://127.0.0.1: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

Verification

Run the full local suite from a second dev-shell terminal while process-compose up is running:

cd backend
phpcs app tests routes database
./vendor/bin/phpunit tests

cd ../frontend/rabbi_gerzi
npm run format
npm run lint
npm run type-check
npm run test:e2e

Run the full Nix package and module checks:

nix flake check

Deployment

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";

                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.

Useful Nix Outputs

Build the frontend package:

nix build .#frontend

Build the backend package:

nix build .#backend

Evaluate all checks:

nix flake check