spread

https://git.tonybtw.com/spread.git git://git.tonybtw.com/spread.git
6,118 bytes raw
1
{
2
  description = "Spread — dev shell: Go, air live-reload, and a local Postgres";
3
4
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
5
6
  outputs = { self, nixpkgs }:
7
    let
8
      systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
9
      forAll = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
10
    in
11
    {
12
      packages = forAll (pkgs: {
13
        default = pkgs.buildGoModule {
14
          pname = "spread";
15
          version = "0.1.0";
16
          src = ./.;
17
          vendorHash = "sha256-YaOsSrH0pG+H+9ntjcyRjUgA6l0wgcg7PksrerMfyGg=";
18
          subPackages = [ "." ];
19
          ldflags = [ "-s" "-w" ];
20
        };
21
      });
22
23
      # Import into a NixOS host and set services.spread.{domain,acmeEmail}.
24
      nixosModules.default = { config, lib, pkgs, ... }:
25
        let cfg = config.services.spread;
26
        in {
27
          options.services.spread = {
28
            enable = lib.mkEnableOption "Spread BOM re-quoting app";
29
            package = lib.mkOption {
30
              type = lib.types.package;
31
              default = self.packages.${pkgs.system}.default;
32
              description = "The spread package to run.";
33
            };
34
            domain = lib.mkOption {
35
              type = lib.types.str;
36
              example = "spread.example.com";
37
              description = "Public hostname served over HTTPS.";
38
            };
39
            acmeEmail = lib.mkOption {
40
              type = lib.types.str;
41
              description = "Contact email for Let's Encrypt.";
42
            };
43
            port = lib.mkOption {
44
              type = lib.types.port;
45
              default = 8137;
46
            };
47
            mailBackend = lib.mkOption {
48
              type = lib.types.enum [ "log" "smtp" "sendgrid" ];
49
              default = "log";
50
            };
51
            environmentFile = lib.mkOption {
52
              type = lib.types.nullOr lib.types.path;
53
              default = null;
54
              description = "Secrets file (SMTP_*, SENDGRID_API_KEY, MAIL_FROM); kept out of the Nix store.";
55
            };
56
          };
57
58
          config = lib.mkIf cfg.enable {
59
            users.users.spread = { isSystemUser = true; group = "spread"; };
60
            users.groups.spread = { };
61
62
            services.postgresql = {
63
              enable = true;
64
              ensureDatabases = [ "spread" ];
65
              ensureUsers = [{ name = "spread"; ensureDBOwnership = true; }];
66
            };
67
68
            systemd.services.spread = {
69
              description = "Spread app";
70
              after = [ "network.target" "postgresql.service" ];
71
              wants = [ "postgresql.service" ];
72
              wantedBy = [ "multi-user.target" ];
73
              environment = {
74
                PORT = toString cfg.port;
75
                DATABASE_URL = "postgresql:///spread?host=/run/postgresql";
76
                SPREAD_BASE_URL = "https://${cfg.domain}";
77
                SPREAD_SECURE_COOKIES = "1";
78
                MAIL_BACKEND = cfg.mailBackend;
79
              };
80
              serviceConfig = {
81
                ExecStart = "${cfg.package}/bin/spread";
82
                User = "spread";
83
                Group = "spread";
84
                Restart = "on-failure";
85
                NoNewPrivileges = true;
86
                ProtectSystem = "strict";
87
                ProtectHome = true;
88
                PrivateTmp = true;
89
              } // lib.optionalAttrs (cfg.environmentFile != null) {
90
                EnvironmentFile = cfg.environmentFile;
91
              };
92
            };
93
94
            security.acme = {
95
              acceptTerms = true;
96
              defaults.email = cfg.acmeEmail;
97
            };
98
99
            services.nginx = {
100
              enable = true;
101
              recommendedProxySettings = true;
102
              recommendedTlsSettings = true;
103
              virtualHosts.${cfg.domain} = {
104
                enableACME = true;
105
                forceSSL = true;
106
                locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
107
              };
108
            };
109
110
            networking.firewall.allowedTCPPorts = [ 80 443 ];
111
          };
112
        };
113
114
      devShells = forAll (pkgs: {
115
        default = pkgs.mkShell {
116
          packages = [ pkgs.go pkgs.air pkgs.postgresql_17 ];
117
118
          # A throwaway Postgres lives under the project dir so `air` can run the
119
          # full app (accounts) without touching any real database.
120
          shellHook = ''
121
            # Keep the dev Postgres OUT of the project tree — a unix socket in
122
            # $PWD breaks `nix build`/`nix develop` (Nix can't copy a socket).
123
            export SPREAD_STATE="''${XDG_STATE_HOME:-$HOME/.local/state}/spread"
124
            export PGDATA="$SPREAD_STATE/pgdata"
125
            export PGHOST="$SPREAD_STATE/sock"
126
            export PGUSER=postgres
127
            export DATABASE_URL="postgres://postgres@/spread?host=$PGHOST"
128
            # Dev-forgiving: serve the public demo even if Postgres isn't running.
129
            export SPREAD_DB_OPTIONAL=1
130
            # Invite / password-reset emails print to the server log in dev.
131
            export MAIL_BACKEND=log
132
            export SPREAD_BASE_URL="http://localhost:8137"
133
134
            pg-start() {
135
              mkdir -p "$PGHOST"
136
              [ -d "$PGDATA" ] || initdb -D "$PGDATA" -U postgres --auth=trust >/dev/null
137
              pg_ctl -D "$PGDATA" -o "-k $PGHOST -c listen_addresses=" -w start
138
              createdb spread 2>/dev/null && echo "created database 'spread'" || true
139
            }
140
            pg-stop() { pg_ctl -D "$PGDATA" stop -m fast; }
141
            spread-admin() {
142
              SPREAD_ADMIN_PASSWORD="''${2:-changeme123}" go run . init-admin "''${1:-admin@spread.test}"
143
            }
144
145
            echo "spread dev shell"
146
            echo "  air           — run with live reload (rebuilds on .go/.html/.js/.sql change)"
147
            echo "  pg-start      — start the local dev Postgres (needed for /login /admin /app)"
148
            echo "  pg-stop       — stop it"
149
            echo "  spread-admin <email> <password>  — create/reset the admin user"
150
          '';
151
        };
152
      });
153
    };
154
}