The Internal Developer Platform Pattern
The Internal Developer Platform Pattern
Fifty repos each pinning their own nixpkgs is fifty llvm builds. The boring default is: one platform flake on nixpkgs 26.05 that exports devShells, templates, and nixosModules, and product flakes follows it.
Mental model
git.desk.internal/platform nixpkgs 26.05
├── devShells.backend / frontend
├── templates.go-service
└── nixosModules.baseline
git.desk.internal/desk-api
inputs.platform.url = …
inputs.nixpkgs.follows = "platform/nixpkgs"
Upgrading Go or sshd defaults is a platform bump, then a lock bump in each consumer (or a bot). Not a meeting. Tag platform-26.05.3; consumers pin the tag, not main. A broken main at 09:01 must not break every laptop.
Keep the platform thin: shells, templates, twenty lines of sshd. An overlay that replaces half of nixpkgs is a fork you cannot debug.
Worked examples
Case 1: Platform flake
Save as flake.nix in the platform repo:
# flake.nix
{
description = "Desk platform";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAll = f: nixpkgs.lib.genAttrs systems (s: f nixpkgs.legacyPackages.${s});
in
{
devShells = forAll (pkgs: {
backend = pkgs.mkShell {
packages = [ pkgs.go pkgs.gopls pkgs.postgresql ];
};
frontend = pkgs.mkShell {
packages = [ pkgs.nodejs pkgs.pnpm ];
};
});
templates.go-service = {
path = ./templates/go-service;
description = "Desk Go service";
};
nixosModules.baseline = ./modules/baseline.nix;
};
}Case 2: Init from template
Save as templates/go-service/flake.nix so init already follows:
# templates/go-service/flake.nix
{
description = "Desk Go service";
inputs.platform.url = "git+ssh://git.desk.internal/platform.git?ref=refs/tags/platform-26.05.3";
inputs.nixpkgs.follows = "platform/nixpkgs";
outputs = { self, nixpkgs, platform }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
devShells.x86_64-linux.default = platform.devShells.x86_64-linux.backend;
packages.x86_64-linux.default = pkgs.hello;
};
}nix flake init -t git+ssh://git.desk.internal/platform.git#go-serviceCase 3: Consumer lock bump
Save as the product flake.nix (desk-api):
# flake.nix
{
description = "desk-api";
inputs.platform.url = "git+ssh://git.desk.internal/platform.git?ref=refs/tags/platform-26.05.3";
inputs.nixpkgs.follows = "platform/nixpkgs";
outputs = { self, nixpkgs, platform }: {
devShells.x86_64-linux.default =
platform.devShells.x86_64-linux.backend;
nixosModules.default = { imports = [ platform.nixosModules.baseline ]; };
};
}nix flake update platform
nix flake metadatanix flake metadata must show one nixpkgs. Two nixpkgs means a follows is missing.
Do not nix flake update of everything on Friday. Update platform, then only what broke.
Case 4: Baseline NixOS module
Save as modules/baseline.nix:
# modules/baseline.nix
{ lib, ... }:
{
services.openssh.enable = lib.mkDefault true;
services.openssh.settings.PasswordAuthentication = false;
networking.firewall.enable = true;
system.stateVersion = lib.mkDefault "26.05";
}Hosts import platform.nixosModules.baseline. Hosts may mkForce in an emergency; review that in the PR. mkDefault is how a host opts into a different stateVersion if it was born earlier — birth still wins.
Case 5: Version the platform
git tag -a platform-26.05.3 -m "26.05.3 shells + sshd"
git push origin platform-26.05.3Consumers pin the tag in inputs.platform.url. Floating main is for the platform team’s laptops, not for production desk-api.
The trap
The trap is a platform that overlays half of nixpkgs. Consumers cannot debug go anymore: is it nixpkgs, the overlay, or the product? Keep the platform thin. One overlay for one pin, documented in README.md.
The boring rule
- One nixpkgs: 26.05 via the platform flake.
- Templates +
follows.nix flake metadatashows a single nixpkgs. nix flake update platformis the upgrade API.- Tags, not floating main, for production consumers.
- Thin modules. No mega-overlay.
Try this
nix flake init -tfrom a local path template.nix flake metadataon a consumer and confirm a single nixpkgs.- Add a
devShells.docswithpkgs.pandocon the platform; consume it from desk-api. - Tag a fake
platform-26.05.3and pin it in a consumer lock.