StackPanel
Dev Environment

Packages & Scripts

Add tools to your shell and define custom commands

Stackpanel gives you two ways to extend your development shell: packages add tools to your PATH, and scripts define project-specific commands that your whole team can use.

Packages

Packages are Nix derivations added to your shell environment. When you enter the dev shell, every package's bin/ directory is on your PATH.

stackpanel.devshell.packages = with pkgs; [
  nodejs
  bun
  jq
  awscli2
  postgresql
];

Packages are pinned to exact versions by your flake.lock. Every team member gets the same node, the same jq, the same aws—regardless of what's installed on their system.

You don't need to install packages globally or manage version managers like nvm or fnm. The dev shell provides exactly the versions you specify, isolated from the rest of the system.

Finding Packages

Search for available packages on search.nixos.org or from the command line:

nix search nixpkgs nodejs
nix search nixpkgs python3

User Packages

Individual developers can add personal packages that don't affect the rest of the team using stackpanel.userPackages:

# .stack/config.local.nix (gitignored)
{
  stackpanel.userPackages = with pkgs; [
    lazygit
    ripgrep
    fd
  ];
}

These are merged into the shell alongside the project packages but don't show up in version control.

Scripts

Scripts are named shell commands available in your dev shell. They're defined in stackpanel.scripts and become executables on your PATH.

You can also opt into Turbo integration so scripts are materialized as a generated workspace package (default: packages/gen/scripts, package name @gen/scripts) and can run through turbo run <script-name> with dependency/caching metadata.

Inline Scripts

For simple commands, use exec to define the script inline:

stackpanel.scripts = {
  dev = {
    exec = "bun run --filter './apps/*' dev";
    description = "Start all development servers";
  };

  db-seed = {
    exec = "bun run seed";
    description = "Seed the database with test data";
  };

  lint = {
    exec = "oxlint . && tsc --noEmit";
    description = "Run linter and type checker";
  };
};

After entering your shell, these are available as regular commands:

$ dev        # starts dev servers
$ db-seed    # seeds the database
$ lint       # runs lint + typecheck

File-Based Scripts

For anything longer than a one-liner, point path at a shell script file:

stackpanel.scripts.deploy = {
  path = ./.stack/src/scripts/deploy.sh;
  description = "Deploy the application";
  runtimeInputs = [ pkgs.awscli2 pkgs.jq ];
  env.AWS_REGION = "us-west-2";
};

File-based scripts get better editor support (syntax highlighting, linting) and are easier to test in isolation. The path and exec options are mutually exclusive—use one or the other.

Runtime Inputs

Use runtimeInputs to add packages to a script's PATH without adding them to the entire shell:

stackpanel.scripts."db:migrate" = {
  exec = "drizzle-kit migrate";
  description = "Run database migrations";
  runtimeInputs = [ pkgs.nodejs ];
};

This is useful when a script needs a tool that you don't want globally available—it keeps the shell clean.

Environment Variables

Pass configuration into scripts via env:

stackpanel.scripts."test:e2e" = {
  exec = "playwright test";
  description = "Run end-to-end tests";
  env = {
    CI = "true";
    BASE_URL = "http://localhost:4200";
  };
};

Environment variables defined in env are exported before the script runs. This is the recommended way to pass Nix-evaluated values into file-based scripts.

Timeouts

Scripts have a default timeout of 5 minutes to prevent runaway processes. You can adjust this per-script:

stackpanel.scripts = {
  "health:check" = {
    exec = "curl -sf http://localhost:4200/health";
    description = "Quick health check";
    timeout = 30;       # 30 seconds
  };

  "db:restore" = {
    path = ./.stack/src/scripts/db-restore.sh;
    description = "Restore database from backup";
    timeout = 1800;     # 30 minutes
  };

  "dev" = {
    exec = "bun run dev";
    description = "Start dev server";
    timeout = 0;        # no timeout (long-running)
  };
};

Common timeout values:

PresetSecondsUse Case
Quick30Health checks, simple commands
Default300Most scripts, network calls, builds
Build900Complex compilations
Deploy1800Deployments, migrations
Long3600Data processing
None0Long-running processes (dev servers)

Namespaced Scripts

Extensions use a colon-separated namespace to avoid name collisions:

stackpanel.scripts = {
  "sst:deploy" = {
    exec = "sst deploy";
    description = "Deploy SST infrastructure";
  };

  "sst:dev" = {
    exec = "sst dev";
    description = "Start SST dev mode";
  };

  "docker:build" = {
    exec = "docker compose build";
    description = "Build Docker images";
  };
};

This convention keeps things tidy as your project grows. Builtin extensions follow this pattern automatically—when you enable the SST extension, its scripts appear as sst:deploy, sst:dev, etc.

How Scripts Are Built

Under the hood, every script becomes a Nix derivation via pkgs.writeShellApplication. All scripts are bundled into a single stackpanel-scripts package with executables in bin/. This means:

  • Scripts are immutable—they can't be accidentally modified at runtime
  • Dependencies declared in runtimeInputs are pinned to exact Nix store paths
  • Scripts run with set -euo pipefail by default (strict mode)
  • The combined scripts package is cached by Nix like any other derivation

Scripts are also exposed as flake outputs, so you can run them outside the dev shell:

nix run .#scripts.db-seed

Turbo Integration

To connect scripts to Turborepo, enable generated package support:

stackpanel.scriptsConfig = {
  generateTurboPackage = true;
  # Optional overrides:
  # turboPackageId = "scripts";
  # turboPackageName = "@gen/scripts";
  # turboPackagePath = "packages/gen/scripts";
};

When enabled, Stackpanel generates/merges a Turbo package declaration for scripts. The resulting workspace package contributes package.json scripts where each script command maps to its script name, so Turbo can execute them directly.

Per-Script Turbo Metadata

Each script can declare Turbo task metadata under turbo:

stackpanel.scripts = {
  "write-files" = {
    exec = "write-files";
    description = "Materialize generated files";
    turbo = {
      enable = true;
      cache = false;
      inputs = [ ".stack/**" "nix/stackpanel/**" ];
    };
  };

  "check-files" = {
    exec = "check-files";
    description = "Verify generated files are current";
    turbo = {
      enable = true;
      dependsOn = [ "write-files" ];
      cache = false;
      inputs = [ ".stack/**" "nix/stackpanel/**" ];
    };
  };

  "build:ui" = {
    exec = "bun run --filter @stackpanel/web build";
    description = "Build web UI";
    turbo = {
      enable = true;
      dependsOn = [ "^build" "check-files" ];
      outputs = [ "apps/web/.next/**" ];
    };
  };
};

Supported turbo fields:

  • enable: Register this script as a Turbo task
  • dependsOn: Task dependencies (supports ^task syntax)
  • outputs: Output globs for caching
  • inputs: Input globs for cache keys
  • cache: Override Turbo cache behavior
  • persistent: Mark long-running tasks
  • interactive: Mark tasks that read stdin

Running Through Turbo

After generating files (write-files) and with Turbo configured, run scripts via:

turbo run write-files
turbo run check-files
turbo run build:ui

This gives you Turbo dependency ordering and cache behavior while keeping script definitions in one place (stackpanel.scripts).

Reference

On this page