{ config, lib, shb, ... }: let cfg = config.shb.karakeep; in { imports = [ ../../lib/module.nix ../blocks/nginx.nix ]; options.shb.karakeep = { enable = lib.mkEnableOption "the Karakeep service"; subdomain = lib.mkOption { type = lib.types.str; description = "Subdomain under which Karakeep will be served."; default = "karakeep"; }; domain = lib.mkOption { type = lib.types.str; description = "domain under which Karakeep will be served."; example = "mydomain.com"; }; ssl = lib.mkOption { description = "Path to SSL files"; type = lib.types.nullOr shb.contracts.ssl.certs; default = null; }; port = lib.mkOption { type = lib.types.port; description = "Port Karakeep listens to incoming requests."; default = 3000; }; environment = lib.mkOption { default = { }; type = lib.types.attrsOf lib.types.str; description = "Extra environment variables. See https://docs.karakeep.app/configuration/"; example = '' { OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}"; INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b"; INFERENCE_IMAGE_MODEL = "llava"; EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5"; INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true"; INFERENCE_JOB_TIMEOUT_SEC = "200"; } ''; }; ldap = lib.mkOption { description = '' Setup LDAP integration. ''; default = { }; type = lib.types.submodule { options = { userGroup = lib.mkOption { type = lib.types.str; description = "Group users must belong to to be able to login."; default = "karakeep_user"; }; }; }; }; sso = lib.mkOption { description = '' Setup SSO integration. ''; default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "SSO integration."; authEndpoint = lib.mkOption { type = lib.types.str; description = "Endpoint to the SSO provider."; example = "https://authelia.example.com"; }; clientID = lib.mkOption { type = lib.types.str; description = "Client ID for the OIDC endpoint."; default = "karakeep"; }; authorization_policy = lib.mkOption { type = lib.types.enum [ "one_factor" "two_factor" ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; sharedSecret = lib.mkOption { description = "OIDC shared secret for Karakeep."; type = lib.types.submodule { options = shb.contracts.secret.mkRequester { owner = "karakeep"; # These services are the ones relying on the environment file containing the secrets. restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ]; }; }; }; sharedSecretForAuthelia = lib.mkOption { description = "OIDC shared secret for Authelia. Must be the same as `sharedSecret`"; type = lib.types.submodule { options = shb.contracts.secret.mkRequester { mode = "0400"; ownerText = "config.shb.authelia.autheliaUser"; owner = config.shb.authelia.autheliaUser; }; }; }; }; }; }; backup = lib.mkOption { description = '' Backup state directory. ''; default = { }; type = lib.types.submodule { options = shb.contracts.backup.mkRequester { user = "karakeep"; sourceDirectories = [ "/var/lib/karakeep" ]; }; }; }; nextauthSecret = lib.mkOption { description = "NextAuth secret."; type = lib.types.submodule { options = shb.contracts.secret.mkRequester { owner = "karakeep"; # These services are the ones relying on the environment file containing the secrets. restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ]; }; }; }; meilisearchMasterKey = lib.mkOption { description = "Master key used to secure communication with Meilisearch."; type = lib.types.submodule { options = shb.contracts.secret.mkRequester { owner = "karakeep"; # These services are the ones relying on the environment file containing the secrets. restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ]; }; }; }; dashboard = lib.mkOption { description = '' Dashboard contract consumer ''; default = { }; type = lib.types.submodule { options = shb.contracts.dashboard.mkRequester { externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; externalUrlText = "https://\${config.shb.karakeep.subdomain}.\${config.shb.karakeep.domain}"; internalUrl = "http://127.0.0.1:${toString cfg.port}"; }; }; }; }; config = ( lib.mkMerge [ (lib.mkIf cfg.enable { services.karakeep = { enable = true; meilisearch.enable = true; extraEnvironment = { PORT = toString cfg.port; DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS } // cfg.environment; }; shb.nginx.vhosts = [ { inherit (cfg) subdomain domain ssl; upstream = "http://127.0.0.1:${toString cfg.port}/"; } ]; # Piggybacking onto the upstream karakeep-init and replacing its script by ours. # This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start # instead of using the value from the cfg.meilisearchMasterKey option. systemd.services.karakeep-init = { script = lib.mkForce ( (shb.replaceSecrets { userConfig = { MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path; NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path; } // lib.optionalAttrs cfg.sso.enable { OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path; }; resultPath = "/var/lib/karakeep/settings.env"; generator = shb.toEnvVar; }) + '' export DATA_DIR="$STATE_DIRECTORY" exec ${config.services.karakeep.package}/lib/karakeep/migrate '' ); }; }) (lib.mkIf cfg.enable { services.meilisearch = { masterKeyFile = cfg.meilisearchMasterKey.result.path; settings = { experimental_dumpless_upgrade = true; env = "production"; }; }; }) (lib.mkIf (cfg.enable && cfg.sso.enable) { shb.lldap.ensureGroups = { ${cfg.ldap.userGroup} = { }; }; shb.authelia.extraOidcAuthorizationPolicies.karakeep = { default_policy = "deny"; rules = [ { subject = [ "group:${cfg.ldap.userGroup}" ]; policy = cfg.sso.authorization_policy; } ]; }; shb.authelia.oidcClients = [ { client_id = cfg.sso.clientID; client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; scopes = [ "openid" "email" "profile" ]; authorization_policy = "karakeep"; redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom" ]; } ]; services.karakeep = { extraEnvironment = { DISABLE_SIGNUPS = "false"; DISABLE_PASSWORD_AUTH = "true"; NEXTAUTH_URL = "https://${cfg.subdomain}.${cfg.domain}"; OAUTH_WELLKNOWN_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration"; OAUTH_PROVIDER_NAME = "Single Sign-On"; OAUTH_CLIENT_ID = cfg.sso.clientID; OAUTH_SCOPE = "openid email profile"; }; }; }) ] ); }