grist.nix 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. { config
  2. , lib
  3. , pkgs
  4. , ...
  5. }:
  6. let
  7. cfg = config.services.grist;
  8. in
  9. {
  10. options.services.grist = {
  11. enable = lib.mkEnableOption "Grist Core self-hosted spreadsheet server";
  12. package = lib.mkPackageOption pkgs "grist-core" { };
  13. port = lib.mkOption {
  14. type = lib.types.port;
  15. default = 8484;
  16. description = "Port on which Grist listens.";
  17. };
  18. dataDir = lib.mkOption {
  19. type = lib.types.path;
  20. default = "/var/lib/grist";
  21. description = "Directory for persistent data (documents, home database, etc.). Equivalent to /persist in Docker.";
  22. };
  23. sandboxFlavor = lib.mkOption {
  24. type = lib.types.enum [ "unsandboxed" "gvisor" "pyodide" "macSandboxExec" ];
  25. default = "unsandboxed";
  26. description = ''
  27. Sandbox flavor for executing Python formulas.
  28. - unsandboxed: No sandbox (fastest, least secure)
  29. - gvisor: gVisor sandbox (Linux only, most secure)
  30. - pyodide: WebAssembly sandbox (cross-platform, experimental)
  31. - macSandboxExec: macOS native sandbox
  32. '';
  33. };
  34. singleOrg = lib.mkOption {
  35. type = lib.types.nullOr lib.types.str;
  36. default = null;
  37. example = "mycompany";
  38. description = "If set, pins Grist to a single organization (simplifies UI, common for self-hosted).";
  39. };
  40. defaultEmail = lib.mkOption {
  41. type = lib.types.str;
  42. default = "[email protected]";
  43. description = "Default admin email used for initial login.";
  44. };
  45. host = lib.mkOption {
  46. type = lib.types.str;
  47. default = "0.0.0.0";
  48. description = "Host interface to bind (0.0.0.0 for all interfaces).";
  49. };
  50. orgInPath = lib.mkOption {
  51. type = lib.types.bool;
  52. default = true;
  53. description = "Include organization in URL path (Docker default).";
  54. };
  55. serveSameOrigin = lib.mkOption {
  56. type = lib.types.bool;
  57. default = true;
  58. description = "Serve home and docs on the same origin/port (Docker default).";
  59. };
  60. openFirewall = lib.mkOption {
  61. type = lib.types.bool;
  62. default = false;
  63. description = "Open the Grist port in the firewall.";
  64. };
  65. environment = lib.mkOption {
  66. type = lib.types.attrsOf lib.types.str;
  67. default = { };
  68. description = "Additional environment variables to pass to Grist.";
  69. };
  70. environmentFile = lib.mkOption {
  71. type = lib.types.nullOr lib.types.path;
  72. default = null;
  73. description = "File containing environment variables (e.g., for secrets like GRIST_SESSION_SECRET).";
  74. };
  75. };
  76. config = lib.mkIf cfg.enable {
  77. systemd.services.grist = {
  78. description = "Grist Core Spreadsheet Server";
  79. wantedBy = [ "multi-user.target" ];
  80. after = [ "network.target" ];
  81. environment = {
  82. PORT = toString cfg.port;
  83. GRIST_HOST = cfg.host;
  84. GRIST_SINGLE_PORT = if cfg.serveSameOrigin then "true" else "false";
  85. GRIST_ORG_IN_PATH = if cfg.orgInPath then "true" else "false";
  86. GRIST_DATA_DIR = "${cfg.dataDir}/docs";
  87. GRIST_INST_DIR = cfg.dataDir;
  88. GRIST_SESSION_COOKIE = "grist_core";
  89. GRIST_SANDBOX_FLAVOR = cfg.sandboxFlavor;
  90. NODE_OPTIONS = "--no-deprecation";
  91. NODE_ENV = "production";
  92. TYPEORM_DATABASE = "${cfg.dataDir}/home.sqlite3";
  93. } // lib.optionalAttrs (cfg.singleOrg != null) {
  94. GRIST_SINGLE_ORG = cfg.singleOrg;
  95. GRIST_DEFAULT_EMAIL = cfg.defaultEmail;
  96. } // cfg.environment;
  97. serviceConfig = {
  98. ExecStart = "${lib.getExe cfg.package}";
  99. DynamicUser = true;
  100. StateDirectory = "grist";
  101. StateDirectoryMode = "0700";
  102. WorkingDirectory = cfg.dataDir;
  103. EnvironmentFile = lib.optionalString (cfg.environmentFile != null) cfg.environmentFile;
  104. Restart = "always";
  105. # Hardening
  106. ProtectSystem = "strict";
  107. ProtectHome = true;
  108. PrivateTmp = true;
  109. NoNewPrivileges = true;
  110. RestrictSUIDSGID = true;
  111. };
  112. preStart = ''
  113. # Ensure data directories exist with correct ownership
  114. mkdir -p ${cfg.dataDir}/docs
  115. chown -R $USER:$GROUP ${cfg.dataDir}
  116. '';
  117. };
  118. networking.firewall = lib.mkIf cfg.openFirewall {
  119. allowedTCPPorts = [ cfg.port ];
  120. };
  121. # Optional: expose the package for passthru.tests or manual use
  122. environment.systemPackages = [ cfg.package ];
  123. };
  124. }