| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142 |
- { config
- , lib
- , pkgs
- , ...
- }:
- let
- cfg = config.services.grist;
- in
- {
- options.services.grist = {
- enable = lib.mkEnableOption "Grist Core self-hosted spreadsheet server";
- package = lib.mkPackageOption pkgs "grist-core" { };
- port = lib.mkOption {
- type = lib.types.port;
- default = 8484;
- description = "Port on which Grist listens.";
- };
- dataDir = lib.mkOption {
- type = lib.types.path;
- default = "/var/lib/grist";
- description = "Directory for persistent data (documents, home database, etc.). Equivalent to /persist in Docker.";
- };
- sandboxFlavor = lib.mkOption {
- type = lib.types.enum [ "unsandboxed" "gvisor" "pyodide" "macSandboxExec" ];
- default = "unsandboxed";
- description = ''
- Sandbox flavor for executing Python formulas.
- - unsandboxed: No sandbox (fastest, least secure)
- - gvisor: gVisor sandbox (Linux only, most secure)
- - pyodide: WebAssembly sandbox (cross-platform, experimental)
- - macSandboxExec: macOS native sandbox
- '';
- };
- singleOrg = lib.mkOption {
- type = lib.types.nullOr lib.types.str;
- default = null;
- example = "mycompany";
- description = "If set, pins Grist to a single organization (simplifies UI, common for self-hosted).";
- };
- defaultEmail = lib.mkOption {
- type = lib.types.str;
- default = "[email protected]";
- description = "Default admin email used for initial login.";
- };
- host = lib.mkOption {
- type = lib.types.str;
- default = "0.0.0.0";
- description = "Host interface to bind (0.0.0.0 for all interfaces).";
- };
- orgInPath = lib.mkOption {
- type = lib.types.bool;
- default = true;
- description = "Include organization in URL path (Docker default).";
- };
- serveSameOrigin = lib.mkOption {
- type = lib.types.bool;
- default = true;
- description = "Serve home and docs on the same origin/port (Docker default).";
- };
- openFirewall = lib.mkOption {
- type = lib.types.bool;
- default = false;
- description = "Open the Grist port in the firewall.";
- };
- environment = lib.mkOption {
- type = lib.types.attrsOf lib.types.str;
- default = { };
- description = "Additional environment variables to pass to Grist.";
- };
- environmentFile = lib.mkOption {
- type = lib.types.nullOr lib.types.path;
- default = null;
- description = "File containing environment variables (e.g., for secrets like GRIST_SESSION_SECRET).";
- };
- };
- config = lib.mkIf cfg.enable {
- systemd.services.grist = {
- description = "Grist Core Spreadsheet Server";
- wantedBy = [ "multi-user.target" ];
- after = [ "network.target" ];
- environment = {
- PORT = toString cfg.port;
- GRIST_HOST = cfg.host;
- GRIST_SINGLE_PORT = if cfg.serveSameOrigin then "true" else "false";
- GRIST_ORG_IN_PATH = if cfg.orgInPath then "true" else "false";
- GRIST_DATA_DIR = "${cfg.dataDir}/docs";
- GRIST_INST_DIR = cfg.dataDir;
- GRIST_SESSION_COOKIE = "grist_core";
- GRIST_SANDBOX_FLAVOR = cfg.sandboxFlavor;
- NODE_OPTIONS = "--no-deprecation";
- NODE_ENV = "production";
- TYPEORM_DATABASE = "${cfg.dataDir}/home.sqlite3";
- } // lib.optionalAttrs (cfg.singleOrg != null) {
- GRIST_SINGLE_ORG = cfg.singleOrg;
- GRIST_DEFAULT_EMAIL = cfg.defaultEmail;
- } // cfg.environment;
- serviceConfig = {
- ExecStart = "${lib.getExe cfg.package}";
- DynamicUser = true;
- StateDirectory = "grist";
- StateDirectoryMode = "0700";
- WorkingDirectory = cfg.dataDir;
- EnvironmentFile = lib.optionalString (cfg.environmentFile != null) cfg.environmentFile;
- Restart = "always";
- # Hardening
- ProtectSystem = "strict";
- ProtectHome = true;
- PrivateTmp = true;
- NoNewPrivileges = true;
- RestrictSUIDSGID = true;
- };
- preStart = ''
- # Ensure data directories exist with correct ownership
- mkdir -p ${cfg.dataDir}/docs
- chown -R $USER:$GROUP ${cfg.dataDir}
- '';
- };
- networking.firewall = lib.mkIf cfg.openFirewall {
- allowedTCPPorts = [ cfg.port ];
- };
- # Optional: expose the package for passthru.tests or manual use
- environment.systemPackages = [ cfg.package ];
- };
- }
|