The Internal Developer Platform Pattern

Updated

September 12, 2026

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

Case 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 metadata

nix 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.3

Consumers 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 metadata shows a single nixpkgs.
  • nix flake update platform is the upgrade API.
  • Tags, not floating main, for production consumers.
  • Thin modules. No mega-overlay.

Try this

  1. nix flake init -t from a local path template.
  2. nix flake metadata on a consumer and confirm a single nixpkgs.
  3. Add a devShells.docs with pkgs.pandoc on the platform; consume it from desk-api.
  4. Tag a fake platform-26.05.3 and pin it in a consumer lock.