{ config, lib, pkgs, ... }: let cfg = config.services.rabbi-gerzi; backendPackage = cfg.backend.package; frontendPackage = cfg.frontend.package; appDir = "${backendPackage}/share/php/rabbi-gerzi-backend"; phpPackage = backendPackage.passthru.php; poolName = "rabbi-gerzi"; storagePath = "${cfg.stateDir}/storage"; cachePath = "${cfg.cacheDir}/bootstrap-cache"; setupService = "rabbi-gerzi-setup"; phpfpmService = "phpfpm-${poolName}"; appEnvironment = { APP_ENV = "production"; APP_DEBUG = "false"; APP_URL = cfg.backend.publicUrl; APP_CONFIG_CACHE = "${cachePath}/config.php"; APP_EVENTS_CACHE = "${cachePath}/events.php"; APP_PACKAGES_CACHE = "${cachePath}/packages.php"; APP_ROUTES_CACHE = "${cachePath}/routes.php"; APP_SERVICES_CACHE = "${cachePath}/services.php"; CACHE_STORE = "file"; CORS_ALLOWED_ORIGINS = lib.concatStringsSep "," cfg.backend.corsAllowedOrigins; DB_CONNECTION = "pgsql"; DB_DATABASE = cfg.database.name; DB_HOST = "/run/postgresql"; DB_PASSWORD = ""; DB_PORT = "5432"; DB_USERNAME = cfg.database.user; FILESYSTEM_DISK = "public"; LARAVEL_STORAGE_PATH = storagePath; LOG_CHANNEL = "single"; MAIL_MAILER = "log"; QUEUE_CONNECTION = "sync"; SESSION_DRIVER = "database"; SESSION_DOMAIN = cfg.backend.cookieDomain; SESSION_SAME_SITE = cfg.backend.cookieSameSite; SESSION_SECURE_COOKIE = lib.boolToString cfg.backend.cookieSecure; }; artisan = "${phpPackage}/bin/php ${appDir}/artisan"; makeDirectoryRule = path: "d ${path} 0750 ${cfg.user} ${cfg.group} - -"; in { options.services.rabbi-gerzi = { enable = lib.mkEnableOption "the Rabbi Gerzi application"; user = lib.mkOption { type = lib.types.str; default = "rabbi-gerzi"; description = "User used to run the Rabbi Gerzi backend."; }; group = lib.mkOption { type = lib.types.str; default = "rabbi-gerzi"; description = "Group used to run the Rabbi Gerzi backend."; }; stateDir = lib.mkOption { type = lib.types.str; default = "/var/lib/rabbi-gerzi"; description = "Writable state directory for uploads and storage."; }; cacheDir = lib.mkOption { type = lib.types.str; default = "/var/cache/rabbi-gerzi"; description = "Writable cache directory for Laravel cache files."; }; database = { name = lib.mkOption { type = lib.types.str; default = cfg.user; description = "PostgreSQL database name."; }; user = lib.mkOption { type = lib.types.str; default = cfg.user; description = "PostgreSQL database user."; }; }; frontend = { hostName = lib.mkOption { type = lib.types.str; description = "Frontend nginx virtual host name."; }; publicUrl = lib.mkOption { type = lib.types.str; default = "https://${cfg.frontend.hostName}"; description = "Public frontend origin."; }; apiBaseUrl = lib.mkOption { type = lib.types.str; default = cfg.backend.publicUrl; description = "API origin baked into the frontend build."; }; package = lib.mkOption { type = lib.types.package; default = pkgs.callPackage ./packages/frontend.nix { inherit (cfg.frontend) apiBaseUrl; }; description = "Frontend package to serve."; }; nginx = lib.mkOption { type = lib.types.attrs; default = { }; description = "Extra nginx virtual host options for the frontend."; }; }; backend = { hostName = lib.mkOption { type = lib.types.str; description = "Backend nginx virtual host name."; }; publicUrl = lib.mkOption { type = lib.types.str; default = "https://${cfg.backend.hostName}"; description = "Public backend origin."; }; environmentFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Runtime dotenv file containing APP_KEY and admin secrets."; }; package = lib.mkOption { type = lib.types.package; default = pkgs.callPackage ./packages/backend.nix { }; description = "Backend package to run."; }; corsAllowedOrigins = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ cfg.frontend.publicUrl ]; description = "Allowed CORS origins for the backend API."; }; cookieSecure = lib.mkOption { type = lib.types.bool; default = true; description = "Whether auth cookies require HTTPS."; }; cookieDomain = lib.mkOption { type = lib.types.str; default = ""; description = "Auth cookie domain. Empty means host-only cookies."; }; cookieSameSite = lib.mkOption { type = lib.types.enum [ "lax" "none" "strict" ]; default = "none"; description = "SameSite value for auth cookies."; }; nginx = lib.mkOption { type = lib.types.attrs; default = { }; description = "Extra nginx virtual host options for the backend."; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.backend.environmentFile != null; message = "services.rabbi-gerzi.backend.environmentFile is required."; } { assertion = cfg.backend.cookieSameSite != "none" || cfg.backend.cookieSecure; message = "SameSite=None auth cookies require cookieSecure = true."; } { assertion = cfg.database.name == cfg.database.user; message = "Managed PostgreSQL database and user names must match."; } ]; users.groups.${cfg.group} = { }; users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; home = cfg.stateDir; }; services.postgresql = { enable = true; ensureDatabases = [ cfg.database.name ]; ensureUsers = [ { name = cfg.database.user; ensureDBOwnership = true; } ]; }; systemd.tmpfiles.rules = map makeDirectoryRule [ cfg.stateDir cfg.cacheDir storagePath "${storagePath}/app" "${storagePath}/app/private" "${storagePath}/app/public" "${storagePath}/framework" "${storagePath}/framework/cache" "${storagePath}/framework/cache/data" "${storagePath}/framework/sessions" "${storagePath}/framework/views" "${storagePath}/logs" cachePath ]; services.phpfpm.pools.${poolName} = { inherit (cfg) user group; inherit phpPackage; settings = { "catch_workers_output" = true; "clear_env" = "no"; "listen.group" = config.services.nginx.group; "listen.mode" = "0660"; "listen.owner" = cfg.user; "pm" = "dynamic"; "pm.max_children" = 16; "pm.max_requests" = 500; "pm.max_spare_servers" = 4; "pm.min_spare_servers" = 1; "pm.start_servers" = 2; }; }; systemd.services.${setupService} = { description = "Prepare Rabbi Gerzi backend"; wantedBy = [ "multi-user.target" ]; before = [ "${phpfpmService}.service" ]; after = [ "postgresql.service" ]; requires = [ "postgresql.service" ]; path = [ phpPackage ]; environment = appEnvironment; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = cfg.user; Group = cfg.group; UMask = "0077"; WorkingDirectory = appDir; EnvironmentFile = cfg.backend.environmentFile; ReadWritePaths = [ cfg.stateDir cfg.cacheDir ]; }; script = '' ${artisan} migrate --force ${artisan} rabbi-gerzi:ensure-initial-admin ''; }; systemd.services.${phpfpmService} = { after = [ "${setupService}.service" ]; requires = [ "${setupService}.service" ]; environment = appEnvironment; serviceConfig = { EnvironmentFile = cfg.backend.environmentFile; ReadWritePaths = [ cfg.stateDir cfg.cacheDir ]; }; }; services.nginx = { enable = lib.mkDefault true; recommendedGzipSettings = lib.mkDefault true; recommendedOptimisation = lib.mkDefault true; recommendedProxySettings = lib.mkDefault true; recommendedTlsSettings = lib.mkDefault true; virtualHosts = { ${cfg.frontend.hostName} = lib.mkMerge [ cfg.frontend.nginx { root = "${frontendPackage}"; locations."/" = { tryFiles = "$uri $uri/ /index.html"; }; } ]; ${cfg.backend.hostName} = lib.mkMerge [ cfg.backend.nginx { root = "${appDir}/public"; locations = { "/" = { index = "index.php"; tryFiles = "$uri $uri/ /index.php?$query_string"; }; "/storage/" = { alias = "${storagePath}/app/public/"; extraConfig = '' expires 30d; access_log off; ''; }; "~ \\.php$" = { extraConfig = '' include ${pkgs.nginx}/conf/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:${config.services.phpfpm.pools.${poolName}.socket}; ''; }; }; } ]; }; }; }; }