On Gaiwan projects the Pattern: Dev Environment Launcher we use is Launchpad (github)

Every Gaiwan project has bin/launchpad as a Pattern: Well Known Entrypoint. This is important for us as a consultancy working across many different projects, even small variations between how different projects are started can mean a high mental overhead to context switching.

Launchpad also offers some aspects of a Pattern: Live Environment, with hot reloading of deps.edn and .env, and their local (non-checked-in) versions deps.local.edn and .env.local. This way dependencies can be added or upgraded on the fly, without a process restart, and configuration settings and secrets can be adjusted. This also makes it possible to enable an additional alias, loading in extra dependencies, without restarting.

Launchpad itself is primarily concerned with starting Clojure and nREPL. It evens out the differences between users of different editors, providing a standard startup mechanism that can cater for Emacs/CIDER, Vim/Conjure, IntelliJ/Cursive, or VS Code/Calva equally. It does admittedly offer the smoothest experience for Emacs users, since Launchpad can instruct Emacs to connect to nREPL once it is up and running, eliminating an additional step, and making the whole process as low friction as a cider-jack-in. We're not aware of similar mechanisms that would enable us to do the same for the other popular editors, but would love to hear about it if there's a way to make it work. As it stands Vim/IntelliJ/VS Code users will have to manually issue a nrepl-connect once the process has booted.

Launchpad is also Shadow-cljs aware, and when so instructed will start a shadow-cljs build as part of the main process.

bin/launchpad itself is a plain Babashka script, it relies on the launchpad dependency being specified in the project's bb.edn, and by default just contains a single line, invoking launchpad with its default sequence of startup steps. This means that, starting out, there is very little boilerplate. This is an important requirement, we want to be able to improve how we start our processes without having to edit dozens of projects.

Since bin/launchpad is just a Babashka script however, it's also the place to put any additional code that's needed to get the project up and running.

  • invoke npm/pnpm install
  • ensure a specific Java version
  • set additional Java/JVM flags
  • download additional resources
  • boot additional services
  • add additional nREPL middlewares (e.g. specific tooling)

That said for projects that use docker-compose we so far have usually kept that out of bin/launchpad, requiring the developer to run docker-compose up separately. We are evaluating what the pros and cons are of possibly rolling this into bin/launchpad as well.

Launchpad provides three mechanisms for local modification of the process. First of all there are standard deps.edn aliases. These can be used to add or drop specific dependencies, they can also influence which shadow builds get started. So frontend developers can use bin/launchpad frontend, while those not running a frontend and thus not in need of a cljs build can leave that alias off. The second mechanism is through deps.local.edn. This gets merged in to the main deps.edn in a similar way to how deps.edn and ~/.clojure/deps.edn get merged. The third mechanism is by setting environment variables in .env.local.

deps.local.edn serves a number of important use cases. The most obvious one is that it allows developers to add dependencies without adding them to deps.edn and checking them in. This could be tools or REPL utility libraries which others on the team don't use, for instance. Or libraries they are evaluating, but haven't yet committed to.

This same mechanism can also be used to override a certain dependency. You might want to switch between versions to pinpoint a regression. More commonly one would switch to a local copy of the library, to work on it in tandem.

We use .env.local as the main mechanism for configuring secrets specifically, preferring configuration files (typically edn), for non-secret configuration flags. For configuration the pattern we default to is to check in config files per environment (resources/<appname>/prod.edn, etc), and to have a config.local.edn at the root for overrides. (We tend to add *.local.* to gitignore).

While EDN files are generally more elegant than environment variables, the truth is that env vars are still the most commonly available mechanism. Especially in cloud environments and in the absence of a persistent file system, often the only way to configure an instance of a service is through env vars. Thus we lean into that, preferring env vars for secrets, and configuring them locally with .env.local.