| 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 |
}
|