- PHP 56.1%
- Vue 22.2%
- TypeScript 14.8%
- Nix 4%
- Shell 1.3%
- Other 1.4%
|
All checks were successful
Update home server / update (push) Successful in 8s
|
||
|---|---|---|
| .forgejo/workflows | ||
| ai | ||
| backend | ||
| frontend/rabbi_gerzi | ||
| nix | ||
| scripts | ||
| .envrc | ||
| .gitignore | ||
| AGENTS.md | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| notes.md | ||
| pilzno_brand_guide.pdf | ||
| process-compose.yml | ||
| README.md | ||
| RECOVERY.md | ||
| social-preview-todo.md | ||
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_KEYencrypts and signs Laravel application data.RABBI_GERZI_INITIAL_ADMIN_EMAILis the first admin login email.RABBI_GERZI_INITIAL_ADMIN_PASSWORDis 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 formatchecks and formats Vue and TypeScript source with oxfmt.npm run lintruns oxlint and eslint.npm run type-checkruns 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:resettask. - Seeded media assets are copied into
backend/storage/app/publicfor 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:
-
Publish and bootstrap the deployment workflow in
home-server-configbefore activating this handoff. Actions must be enabled in both repositories and thelinuxrunner must be available to Rabbi Gerzi. -
Generate a dedicated deploy key outside the repository:
ssh-keygen -t ed25519 -N '' -C rabbi-gerzi-ci \ -f ~/.ssh/rabbi-gerzi-home-server -
Add the
.pubfile to the server repository's deploy keys with write access. Its branch protection must allow this key to push tomaster. -
Store the complete private key as
HOME_SERVER_CONFIG_SSH_KEYin Rabbi Gerzi's Actions secrets. This is separate from the server'sDEPLOY_SSH_KEYused for deployment. -
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:
- Open Websites, select Manage Site, and open Performance.
- Enable Cloudflare.
- 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.
- Confirm that Cloudflare covers
rabbigerzi.com,www.rabbigerzi.com, andapi.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.