{ description = "flashsharp — self-hosted flashcards with a remote MCP surface"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: (flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; flashsharp = pkgs.buildDotnetModule { pname = "flashsharp"; version = "0.1.0"; src = ./.; projectFile = "src/Flashsharp/Flashsharp.csproj"; nugetDeps = ./nuget-deps.json; dotnet-sdk = pkgs.dotnet-sdk_10; dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_10_0; selfContainedBuild = false; executables = [ "flashsharp" ]; meta.mainProgram = "flashsharp"; }; in { packages.default = flashsharp; packages.flashsharp = flashsharp; devShells.default = pkgs.mkShell { packages = with pkgs; [ dotnet-sdk_10 postgresql curl jq ]; }; } )) // { nixosModules.default = { config, pkgs, lib, ... }: let cfg = config.services.flashsharp; package = cfg.package; in { options.services.flashsharp = { enable = lib.mkEnableOption "flashsharp"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.default; description = "flashsharp package to run."; }; host = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "Address Kestrel binds to."; }; port = lib.mkOption { type = lib.types.port; default = 7015; description = "HTTP port."; }; databaseUrl = lib.mkOption { type = lib.types.str; default = "Host=/run/postgresql;Database=flashsharp;Username=flashsharp"; description = '' Npgsql connection string. The database and role must already exist; add them to services.postgresql.ensureDatabases / ensureUsers. ''; }; seedUsername = lib.mkOption { type = lib.types.str; default = "admin"; description = "Username created on the first run when no users exist."; }; seedPasswordHash = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = '' Bcrypt hash of the seed password. Preferred over a plaintext file. Generate with: htpasswd -bnBC 12 "" 'your-password' | cut -d: -f2 ''; }; seedPasswordFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Runtime file containing the plaintext seed password. Only needed on the first run, when the user table is empty. It is read through systemd credentials. Do not point this at a path inside the Nix store, because store paths are world-readable. ''; }; environmentFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Optional extra environment file for the service."; }; }; config = lib.mkIf cfg.enable { users.groups.flashsharp = { }; users.users.flashsharp = { isSystemUser = true; group = "flashsharp"; home = "/var/lib/flashsharp"; }; systemd.services.flashsharp = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" "postgresql.service" ]; requires = [ "postgresql.service" ]; environment = { FLASHSHARP_DB = cfg.databaseUrl; FLASHSHARP_HOST = cfg.host; FLASHSHARP_PORT = toString cfg.port; FLASHSHARP_SEED_USERNAME = cfg.seedUsername; FLASHSHARP_DATA_DIR = "/var/lib/flashsharp"; } // lib.optionalAttrs (cfg.seedPasswordHash != null) { FLASHSHARP_SEED_PASSWORD_HASH = cfg.seedPasswordHash; } // lib.optionalAttrs (cfg.seedPasswordFile != null) { FLASHSHARP_SEED_PASSWORD_FILE = "%d/seed-password"; }; serviceConfig = { User = "flashsharp"; Group = "flashsharp"; StateDirectory = "flashsharp"; StateDirectoryMode = "0700"; UMask = "0077"; WorkingDirectory = "/var/lib/flashsharp"; ExecStart = "${package}/bin/flashsharp"; Restart = "always"; RestartSec = 2; } // lib.optionalAttrs (cfg.seedPasswordFile != null) { LoadCredential = "seed-password:${cfg.seedPasswordFile}"; } // lib.optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; }; }; }; }; }; }