diff --git a/flake.nix b/flake.nix index d36bf53..c8fed26 100644 --- a/flake.nix +++ b/flake.nix @@ -4,69 +4,50 @@ utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, utils }: - let - perSystem = utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in { - packages = { - tide-backend = pkgs.callPackage ./nix/packages/backend.nix { }; - tide-frontend = pkgs.callPackage ./nix/packages/frontend.nix { - apiUrl = "https://apitide.yisroelbaum.com"; - }; - }; + utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + onefetch + php + phpPackages.composer + phpPackages.php-codesniffer + vscode-langservers-extracted + sqlite + nodejs + nixfmt-rfc-style + nixfmt-tree + cypress + yaml-language-server + typescript + postgresql + process-compose + mailpit + ]; - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - onefetch - php - phpPackages.composer - phpPackages.php-codesniffer - vscode-langservers-extracted - sqlite - nodejs - nixfmt-rfc-style - nixfmt-tree - cypress - yaml-language-server - typescript - postgresql - process-compose - mailpit - ]; + shellHook = '' + # Anchor PGDATA to the repo root so subshells in subdirs + # (e.g. backend/) reuse the same cluster instead of seeding + # a stray .postgres there. + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + export PGDATA="$REPO_ROOT/.postgres" + export PGHOST="$PGDATA" + export PGUSER="postgres" + export PGDATABASE="postgres" - shellHook = '' - # Anchor PGDATA to the repo root so subshells in subdirs - # (e.g. backend/) reuse the same cluster instead of seeding - # a stray .postgres there. - REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" - export PGDATA="$REPO_ROOT/.postgres" - export PGHOST="$PGDATA" - export PGUSER="postgres" - export PGDATABASE="postgres" + if [ ! -d "$PGDATA" ]; then + echo "[pg] initializing cluster at $PGDATA" + initdb --auth=trust --username=postgres --no-locale --encoding=UTF8 >/dev/null + { + echo "listen_addresses = '127.0.0.1'" + echo "unix_socket_directories = '$PGDATA'" + } >> "$PGDATA/postgresql.conf" + fi - if [ ! -d "$PGDATA" ]; then - echo "[pg] initializing cluster at $PGDATA" - initdb --auth=trust --username=postgres --no-locale --encoding=UTF8 >/dev/null - { - echo "listen_addresses = '127.0.0.1'" - echo "unix_socket_directories = '$PGDATA'" - } >> "$PGDATA/postgresql.conf" - fi - - echo "[dev] run 'process-compose up' to start postgres + backend + vite" - ''; - }; - }); - in - perSystem // { - nixosModules.tide = import ./nix/module.nix { inherit self; }; - - overlays.default = final: prev: { - tide-backend = final.callPackage ./nix/packages/backend.nix { }; - tide-frontend = final.callPackage ./nix/packages/frontend.nix { - apiUrl = "https://apitide.yisroelbaum.com"; + echo "[dev] run 'process-compose up' to start postgres + backend + vite" + ''; }; - }; - }; + }); } diff --git a/nix/module.nix b/nix/module.nix deleted file mode 100644 index 2fa939f..0000000 --- a/nix/module.nix +++ /dev/null @@ -1,328 +0,0 @@ -{ self }: -{ config, lib, pkgs, ... }: -let - cfg = config.services.tide; - - defaultBackend = self.packages.${pkgs.system}.tide-backend; - defaultFrontend = self.packages.${pkgs.system}.tide-frontend.override { - apiUrl = "https://${cfg.apiDomain}"; - }; - - # The Laravel package lives in the Nix store (read-only). Laravel - # needs a writable storage/ and bootstrap/cache/. We materialize a - # writable copy at /var/lib/tide/app whose contents are symlinks - # back into the store, except for storage/ and bootstrap/cache/ - # which are real writable directories under /var/lib/tide/state. - appRoot = "/var/lib/tide/app"; - stateRoot = "/var/lib/tide/state"; - - poolName = "tide"; - fpmSocket = "/run/phpfpm/${poolName}.sock"; -in -{ - options.services.tide = { - enable = lib.mkEnableOption "TIDE blogging platform"; - - domain = lib.mkOption { - type = lib.types.str; - example = "tide.example.com"; - description = "Domain serving the Vue frontend."; - }; - - apiDomain = lib.mkOption { - type = lib.types.str; - example = "apitide.example.com"; - description = "Domain serving the Laravel backend API."; - }; - - user = lib.mkOption { - type = lib.types.str; - default = "tide"; - description = "Unix user the Laravel process runs as."; - }; - - group = lib.mkOption { - type = lib.types.str; - default = "tide"; - description = "Unix group the Laravel process runs as."; - }; - - database = { - name = lib.mkOption { - type = lib.types.str; - default = "tide"; - description = "PostgreSQL database name."; - }; - user = lib.mkOption { - type = lib.types.str; - default = "tide"; - description = "PostgreSQL role used by Laravel."; - }; - }; - - secretsFile = lib.mkOption { - type = lib.types.path; - description = '' - Path to a file containing environment variables for the - Laravel pool. Must define APP_KEY at minimum. Read at - service start, never copied into the Nix store. - ''; - }; - - backendPackage = lib.mkOption { - type = lib.types.package; - default = defaultBackend; - defaultText = lib.literalExpression - "self.packages.\${pkgs.system}.tide-backend"; - description = "The Laravel backend derivation."; - }; - - frontendPackage = lib.mkOption { - type = lib.types.package; - default = defaultFrontend; - defaultText = lib.literalExpression - "self.packages.\${pkgs.system}.tide-frontend"; - description = "The Vue frontend derivation (built static dist)."; - }; - - nginx = { - useACMEHost = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "example.com"; - description = '' - Reuse an existing wildcard ACME cert (set this to the - apex domain whose cert covers both subdomains). When - null, each vhost requests its own cert via - enableACME = true. - ''; - }; - }; - }; - - config = lib.mkIf cfg.enable { - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - home = "/var/lib/tide"; - }; - users.groups.${cfg.group} = { }; - - systemd.tmpfiles.rules = [ - "d /var/lib/tide 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot} 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/app 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/app/public 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/framework 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/framework/cache 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/framework/sessions 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/framework/views 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/framework/testing 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/storage/logs 0750 ${cfg.user} ${cfg.group} -" - "d ${stateRoot}/bootstrap-cache 0750 ${cfg.user} ${cfg.group} -" - ]; - - # Materialize the writable app root by symlinking from the - # store package, then redirecting the two mutable subtrees - # to /var/lib/tide/state. - systemd.services.tide-prepare = { - description = "Prepare TIDE Laravel app root"; - wantedBy = [ "multi-user.target" ]; - before = [ - "phpfpm-${poolName}.service" - "tide-migrate.service" - ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - set -eu - rm -rf ${appRoot} - mkdir -p ${appRoot} - for entry in ${cfg.backendPackage}/share/php/tide-backend/*; do - name="$(basename "$entry")" - case "$name" in - storage|bootstrap) - ;; - *) - ln -s "$entry" "${appRoot}/$name" - ;; - esac - done - # bootstrap dir: symlink everything from the store - # except cache/ which must be writable. - mkdir -p ${appRoot}/bootstrap - for entry in ${cfg.backendPackage}/share/php/tide-backend/bootstrap/*; do - name="$(basename "$entry")" - if [ "$name" != "cache" ]; then - ln -sf "$entry" "${appRoot}/bootstrap/$name" - fi - done - ln -sfn ${stateRoot}/bootstrap-cache ${appRoot}/bootstrap/cache - ln -sfn ${stateRoot}/storage ${appRoot}/storage - - chown -R ${cfg.user}:${cfg.group} ${appRoot} ${stateRoot} - ''; - }; - - # Postgres - services.postgresql = { - enable = true; - ensureDatabases = [ cfg.database.name ]; - ensureUsers = [ - { - name = cfg.database.user; - ensureDBOwnership = true; - } - ]; - }; - - # PHP-FPM pool - services.phpfpm.pools.${poolName} = { - user = cfg.user; - group = cfg.group; - phpPackage = cfg.backendPackage.passthru.php; - phpEnv = { - APP_ENV = "production"; - APP_DEBUG = "false"; - APP_URL = "https://${cfg.apiDomain}"; - LOG_CHANNEL = "stderr"; - DB_CONNECTION = "pgsql"; - DB_HOST = "/run/postgresql"; - DB_PORT = "5432"; - DB_DATABASE = cfg.database.name; - DB_USERNAME = cfg.database.user; - SESSION_DRIVER = "database"; - CACHE_STORE = "database"; - QUEUE_CONNECTION = "database"; - }; - settings = { - "listen.owner" = config.services.nginx.user; - "listen.group" = config.services.nginx.group; - "listen.mode" = "0660"; - "pm" = "dynamic"; - "pm.max_children" = 16; - "pm.start_servers" = 2; - "pm.min_spare_servers" = 2; - "pm.max_spare_servers" = 4; - "pm.max_requests" = 500; - "catch_workers_output" = true; - "decorate_workers_output" = false; - "clear_env" = false; - }; - }; - - # Pull APP_KEY, DB_PASSWORD, MAIL_*, etc. from the secrets - # file at start time. - systemd.services."phpfpm-${poolName}" = { - after = [ - "tide-prepare.service" - "postgresql.service" - ]; - requires = [ - "tide-prepare.service" - ]; - serviceConfig = { - EnvironmentFile = cfg.secretsFile; - }; - }; - - # One-shot migrations + cache warm. - systemd.services.tide-migrate = { - description = "Run TIDE Laravel migrations and cache warm"; - wantedBy = [ "multi-user.target" ]; - after = [ - "tide-prepare.service" - "postgresql.service" - ]; - requires = [ - "tide-prepare.service" - "postgresql.service" - ]; - before = [ "phpfpm-${poolName}.service" ]; - serviceConfig = { - Type = "oneshot"; - User = cfg.user; - Group = cfg.group; - EnvironmentFile = cfg.secretsFile; - WorkingDirectory = appRoot; - }; - environment = { - APP_ENV = "production"; - APP_DEBUG = "false"; - APP_URL = "https://${cfg.apiDomain}"; - DB_CONNECTION = "pgsql"; - DB_HOST = "/run/postgresql"; - DB_PORT = "5432"; - DB_DATABASE = cfg.database.name; - DB_USERNAME = cfg.database.user; - }; - script = '' - ${cfg.backendPackage.passthru.php}/bin/php artisan migrate --force --no-interaction - ${cfg.backendPackage.passthru.php}/bin/php artisan storage:link --force || true - ${cfg.backendPackage.passthru.php}/bin/php artisan config:cache - ${cfg.backendPackage.passthru.php}/bin/php artisan route:cache - ''; - }; - - # nginx vhosts - services.nginx = { - enable = true; - virtualHosts = { - "${cfg.apiDomain}" = - let - sslAttrs = - if cfg.nginx.useACMEHost != null then { - forceSSL = true; - useACMEHost = cfg.nginx.useACMEHost; - } else { - forceSSL = true; - enableACME = true; - }; - in - sslAttrs // { - root = "${appRoot}/public"; - locations = { - "/" = { - tryFiles = "$uri $uri/ /index.php?$query_string"; - }; - "~ \\.php$" = { - extraConfig = '' - fastcgi_pass unix:${fpmSocket}; - fastcgi_index index.php; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - include ${pkgs.nginx}/conf/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_param HTTPS on; - ''; - }; - "~ /\\.ht" = { - extraConfig = "deny all;"; - }; - }; - }; - - "${cfg.domain}" = - let - sslAttrs = - if cfg.nginx.useACMEHost != null then { - forceSSL = true; - useACMEHost = cfg.nginx.useACMEHost; - } else { - forceSSL = true; - enableACME = true; - }; - in - sslAttrs // { - root = "${cfg.frontendPackage}"; - locations."/" = { - tryFiles = "$uri $uri/ /index.html"; - }; - }; - }; - }; - }; -} diff --git a/nix/packages/backend.nix b/nix/packages/backend.nix deleted file mode 100644 index c662526..0000000 --- a/nix/packages/backend.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ php84, lib }: -let - php = php84; -in -php.buildComposerProject (finalAttrs: { - pname = "tide-backend"; - version = "0.1.0"; - - src = lib.cleanSource ../../backend; - - composerNoDev = true; - composerNoPlugins = true; - composerNoScripts = true; - - vendorHash = "sha256-OYpfX435tPJqiOzQPpWPXCVH1rTeQ74dGuNVyk6+c1A="; - - passthru = { - inherit php; - }; - - meta = { - description = "TIDE Laravel backend"; - license = lib.licenses.mit; - }; -}) diff --git a/nix/packages/frontend.nix b/nix/packages/frontend.nix deleted file mode 100644 index 8ed9d35..0000000 --- a/nix/packages/frontend.nix +++ /dev/null @@ -1,33 +0,0 @@ -{ buildNpmPackage, lib, nodejs_22, apiUrl }: -buildNpmPackage (finalAttrs: { - pname = "tide-frontend"; - version = "0.0.0"; - - src = lib.cleanSource ../../frontend/blog_portal; - - nodejs = nodejs_22; - - npmDepsFetcherVersion = 2; - npmDepsHash = "sha256-NHAo9Bvg80W2341yPaw97khUCJyr/7fyQFvhQFKWYnY="; - - env = { - VITE_API_URL = apiUrl; - # Cypress' postinstall reaches out to download.cypress.io, which - # is not allowed inside the Nix sandbox. Cypress is only used - # for local E2E so skip the download during the production - # build. - CYPRESS_INSTALL_BINARY = "0"; - }; - - installPhase = '' - runHook preInstall - mkdir -p $out - cp -r dist/* $out/ - runHook postInstall - ''; - - meta = { - description = "TIDE Vue frontend (blog_portal)"; - license = lib.licenses.mit; - }; -})