Setup

The history of how I've gotten to this point is winding and many decades long. But the end result is that I use two major tools for setting up my projects in a consistent manner: Direnv and Nix Flakes.

The main reason is history. I have projects that are twenty years old but they are based on languages that never stop changing. In some cases, such as the highly fluid NPM ecosystem, being off by a month can require days of refactoring to handle the current state of deprecations, shifting packages, and trying to get everything playing with each other again.

Nix Flakes help keep me “pinned” to specific versions and reduces the amount of refactoring I have to do to jump into a project, do a few changes, and finish up.

Flakes

Since flakes give me the consistency I need, that means many of my more recent projects have a flake.nix and a flake.lock associated with them. These will have a devShell which makes sure the programs to use the project are available.

{
  inputs = {
    nixpkgs.url = "nixpkgs/nixos-24.11";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils, mfgames-project-setup, mfgames-writing-setup }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShell = pkgs.mkShell {
          packages = [ pkgs.scc ];
        };
      });
}

I try to avoid using the “unstable” branch for NixOS but sometimes that isn't avoidable.

mfgames-project-setup

To simplify my common project layout, I created a setup flake that does some initial configuration including setting up commit linting (since I useconventional commits), formatting, and set up some of the boilerplate files.

{
  inputs = {
    nixpkgs.url = "nixpkgs/nixos-24.11";
    flake-utils.url = "github:numtide/flake-utils";
    mfgames-project-setup.url = "git+https://src.mfgames.com/nixos-contrib/mfgames-project-setup-flake.git";
  };

  outputs = { self, nixpkgs, flake-utils, mfgames-project-setup }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        project-config = mfgames-project-setup.lib.mkConfig {
          inherit system;
          pkgs = nixpkgs.legacyPackages.${system};
          prettier.proseWrap = "never";
        };
      in
      {
        devShell = pkgs.mkShell {
          packages = [ ]
            ++ project-config.packages;

          shellHook = project-config.shellHook;
        };
      });
}

I'm pretty proud of that one, since it basically does 90% of my setup when starting a new project. There is also a second setup flake that sets up my writing projects including getting the right tools installed.

Direnv

To make it easier to get into the environment set up above, I use direnv. My shell is set up to automatically include this, and it sets up paths.

use flake || use nix

dotenv_if_exists

PATH_add node_modules/.bin # NPM
PATH_add target/debug # Rust
PATH_add .local/bin

The use flake command above will automatically enter the environment configured by flake.nix and makes sure I have access to all the tools I need for a given project.

Environment

There are two places I set up environment variables.

For persisted ones, I put them into the .envrc file which is checked into the code. This is used by projects like Fiss to set the local cache file to the one inside the repository and to use the project-local configuration instead of using (and possibly corrupting) my day-to-day one.

For ones not checked in, I use .env to set those variables. This is where I put things like AWS authentication or Forgejo tokens. My Justfile is set up to avoid removing those when I do a just clean and it also loads them using set dotenv-load.