Repository: railwayapp/nixpacks
Pull request #513 (2022-09-08 21:47:45)

Experimental file-based configuration
This PR implements the beginnings of https://github.com/railwayapp/nixpacks/discussions/508. The main features are

- File-based configuration with a `nixpacks.toml` or `nixpacks.json` file
- Consistent configuration via the CLI flags, environment variables, and file
- Build plan merging
- Pin to nixpacks archive for all Nix installs
- Phases are stored as an object on the build plan
- Procfile support is implemented as a provider
- `nixpacks plan . -f toml` to output the plan in toml format

When using file-based config, a warning will appear mentioning that it is experimental and may change. It is marked as experimental so that we can have breaking changes and not be locked into a format just yet.

![image](https://user-images.githubusercontent.com/3044853/189231109-5e657b0a-aafa-4e39-a546-8f59486fd855.png)

An example configuration file is

```toml
[phases.setup]
# Extend what Nix packages get installed
nixPkgs = ["...", "cowsay"]

[phases.build]
# Override the build command
cmds = ["yarn run client:build"]

[start]
cmd = "yarn run client:start"
```

With this change, a `providers` field has been added to the build plan. Zero or one provider may be used at the moment. This allows you to specify that
No automatic detection or build plan should be used. Only the config from the `nixpacks.toml` file will be use
```toml
providers = []
```

Specify exactly what provider should be used
```toml
providers = ["python"]
```

If `providers` is `None`, then the default automatic detection will be used.

Fixes https://github.com/railwayapp/nixpacks/issues/494


Patch:
diff --git a/README.md b/README.md
index e515f3f..59baed6 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 
 Nixpacks takes a source directory and produces an OCI compliant image that can be deployed anywhere. The project was started by the [Railway](https://railway.app) team as an alternative to [Buildpacks](https://buildpacks.io/) and attempts to address a lot of the shortcomings and issues that occurred when deploying thousands of user apps to the Railway platform. The biggest change is that system and language dependencies are pulled from the Nix ecosystem.
 
-Read the docs 👉 [nixpacks.com](https://nixpacks.com)
+Read the docs 👉 [nixpacks.com](https://nixpacks.com).
 
 ## Contributing
 
diff --git a/docs/pages/_app.tsx b/docs/pages/_app.tsx
index 70bd714..aaca6c6 100644
--- a/docs/pages/_app.tsx
+++ b/docs/pages/_app.tsx
@@ -6,6 +6,8 @@ import { Hero, SideNav, TableOfContents, TopNav } from "../components";
 
 // Import other Prism themes here
 import "prismjs/components/prism-bash.min";
+import "prismjs/components/prism-toml.min";
+import "prismjs/components/prism-json.min";
 import "../public/prism-one-light.css";
 
 import "../public/globals.css";
diff --git a/docs/pages/docs/cli.md b/docs/pages/docs/cli.md
index 346dcf4..0265be2 100644
--- a/docs/pages/docs/cli.md
+++ b/docs/pages/docs/cli.md
@@ -57,6 +57,8 @@ For example,
 nixpacks plan examples/node
 ```
 
+By default, the plan is output in JSON format. You can output in TOML format with the `--format toml` option.
+
 View all plan options with
 
 ```sh
diff --git a/docs/pages/docs/configuration/caching.md b/docs/pages/docs/configuration/caching.md
new file mode 100644
index 0000000..189ca06
--- /dev/null
+++ b/docs/pages/docs/configuration/caching.md
@@ -0,0 +1,15 @@
+---
+title: Caching
+---
+
+# {% $markdoc.frontmatter.title %}
+
+By default Nixpacks providers will cache directories during the install and build phases. The specific directories are provider specific but are typically used to speed up installs (e.g. `~/.npm`) and builds (e.g. `~/.cache/go-build`). The contents of these directories are restored before the install/build phases are run and cleared afterwards. This means that the contents of the cached directories **do not appear in the final image**.
+
+The default cache identifier is a hash of the absolute path to the directory being built. This means that subsequent builds of the same directory will be faster out of the box. You can override the cache identifier by passing a `--cache-key` value to the `build` command.
+
+Caching can be disabled entirely by passing `--no-cache`.
+
+Passing`--inline-cache` will write cache metadata into the output image.
+
+Using previous image -created with inline cache enabled- as a cache source, Can be achieved by passing `--cache-from`.
diff --git a/docs/pages/docs/config.md b/docs/pages/docs/configuration/environment.md
similarity index 51%
rename from docs/pages/docs/config.md
rename to docs/pages/docs/configuration/environment.md
index 41c80ef..e388576 100644
--- a/docs/pages/docs/config.md
+++ b/docs/pages/docs/configuration/environment.md
@@ -1,12 +1,10 @@
 ---
-title: Configuration
+title: Environment
 ---
 
 # {% $markdoc.frontmatter.title %}
 
-## Environment Variables
-
-Nixpacks can be configured via environment variables. Most of these variables are prefixed with `NIXPACKS_`.
+Nixpacks can be configured via environment variables. All of these variables are prefixed with `NIXPACKS_`.
 
 | Variable                      | Description                                                                                  |
 | :---------------------------- | :------------------------------------------------------------------------------------------- |
@@ -19,23 +17,3 @@ Nixpacks can be configured via environment variables. Most of these variables ar
 | `NIXPACKS_INSTALL_CACHE_DIRS` | Add additional directories to cache during the install phase                                 |
 | `NIXPACKS_BUILD_CACHE_DIRS`   | Add additional directories to cache during the build phase                                   |
 | `NIXPACKS_NO_CACHE`           | Disable caching for the build                                                                |
-
-## Procfiles
-
-The standard Procfile format is supported by Nixpacks. However, only a single process is supported. The command specified in the Procfile will override the provider start command.
-
-```
-web: npm run start
-```
-
-## Caching
-
-By default Nixpacks providers will cache directories during the install and build phases. The specific directories are provider specific but are typically used to speed up installs (e.g. `~/.npm`) and builds (e.g. `~/.cache/go-build`). The contents of these directories are restored before the install/build phases are run and cleared afterwards. This means that the contents of the cached directories **do not appear in the final image**.
-
-The default cache identifier is a hash of the absolute path to the directory being built. This means that subsequent builds of the same directory will be faster out of the box. You can override the cache identifier by passing a `--cache-key` value to the `build` command.
-
-Caching can be disabled entirely by passing `--no-cache`.
-
-Passing`--inline-cache` will write cache metadata into the output image.
-
-Using previous image -created with inline cache enabled- as a cache source, Can be achieved by passing `--cache-from`
\ No newline at end of file
diff --git a/docs/pages/docs/configuration/file.md b/docs/pages/docs/configuration/file.md
new file mode 100644
index 0000000..2750d6f
--- /dev/null
+++ b/docs/pages/docs/configuration/file.md
@@ -0,0 +1,222 @@
+---
+title: Configuration File
+---
+
+# {% $markdoc.frontmatter.title %}
+
+Nixpacks has **experimental** support for specifying build configuration in a `nixpacks.toml` or `nixpacks.json` file. The config will automatically be used if one of these files is found in the app root. Otherwise, the file can be specified with the `--config <file>` flag.
+
+The contents of this file can contain a full build plan, which means that every aspect of the build can be customized. An example config looks something like:
+
+```toml
+# nixpacks.toml
+
+[phases.setup]
+nixPkgs = ['...', 'cowsay']
+
+[phases.build]
+cmds = ['yarn run server:build']
+
+[start]
+cmd = 'yarn run server:start'
+```
+
+The file-based build plan is merged with the build plan generated by the provider, as well as the configuration from the environment variables and CLI flags. By default values are overridden when merged if they are not null. The configuration priority is
+
+- Provider (lowest)
+- File
+- Environment
+- CLI
+
+## Array Extending
+
+The default behaviour when merging build plans is for a non-null value of higher priority to override a lower priority value. However, you can use the `"..."` special syntax to _extend_ the values in an array. For example:
+
+```toml
+# Build plan from the provider
+[phases.setup]
+nixPkgs = ['nodejs', 'yarn']
+
+# nixpacks.toml
+[phases.setup]
+nixPkgs = ['...', 'cowsay']
+
+# Merged plan
+[phases.setup]
+nixPkgs = ['nodejs', 'yarn', 'cowsay']
+```
+
+The `"..."` represents a hole that will be populated by the values from plan that is merged into.
+
+## Build image
+
+The image to use as the base when building the application.
+
+```toml
+build_image = 'ghcr.io/railwayapp/nixpacks:latest'
+```
+
+## Variables
+
+Key-value pairs of variables to include in the final image.
+
+```toml
+[variables]
+NODE_ENV = 'production'
+HELLO = 'world'
+```
+
+## Static assets
+
+Files that are copied into the `/assets` directory of the image.
+
+```toml
+[staticAssets]
+myFile = '''
+  asdfasdfasdf
+'''
+```
+
+## Phases
+
+The phases specify exactly how the application is built and packaged into an image. Each phase can depend on a list of other phases and the ordering is resolved when the `Dockerfile` is automatically generated and run. The phases are typically defined as
+
+- `setup`: Install the Nix/Apt packages (e.g. NodeJS, Yarn)
+- `install`: Install the language/framework dependencies (e.g. Yarn install)
+- `build`: Build the application (e.g. Yarn build)
+
+However, there is no restriction on the names of phases or what they are named.
+
+```toml
+[phases.setup]
+# ...
+
+[phases.install]
+# ...
+
+[phases.build]
+# ...
+```
+
+### Commands
+
+Array of commands to run.
+
+```toml
+[phase.name]
+  cmds = ['cmd1', 'cmd2']
+```
+
+### Nix packages
+
+Nix packages to install. Available packages can be found at [search.nixos.org](https://search.nixos.org/packages?channel=unstable).
+
+```toml
+[phase.name]
+  nixPkgs = ['cowsay']
+```
+
+### Nix libraries
+
+Nix packages to be made available through the `LD_LIBRARY_PATH` environment variables. The paths to each packages library files are appended.
+
+```toml
+[phase.name]
+  nixLibs = ['zlib']
+```
+
+### Nix overlays
+
+[Nix overlays](https://nixos.wiki/wiki/Overlays) to use as alternate package sources.
+
+```toml
+[phase.name]
+  nixOverlays = ['https://github.com/oxalica/rust-overlay/archive/master.tar.gz']
+```
+
+### Nixpkgs archive
+
+Specific version of the Nixpkgs archive to use. By default all builds are built using the version defined [here](https://github.com/railwayapp/nixpacks/blob/6dc1e66e3d0840230def277d19890cd0da4584d3/src/nixpacks/plan/generator.rs#L16). But this value can be overridden to install Nix packages from an older archive.
+
+```toml
+[phase.name]
+  nixpkgsArchive = '21de2b973f9fee595a7a1ac4693efff791245c34'
+```
+
+### Apt packages
+
+List of packages to install with `apt-get`
+
+```toml
+[phase.name]
+  aptPkgs = ['wget']
+```
+
+### Phase dependencies
+
+List of phases that this phase must run after.
+
+```toml
+[phase.name]
+  dependsOn = ['install']
+```
+
+### Cache directories
+
+Directories to cache for this phase. Cached directories do not appear in the final build. See [/caching](/docs/configuration/caching) for more information.
+
+```toml
+[phase.name]
+  cacheDirectories = ['node_modules/.cache']
+```
+
+### Included files
+
+Files to **only** make available when running this phase. If no array is specified, then all files are copied into the image before running the commands. This can be useful when optimizing the Docker layer cache.
+
+```toml
+[phase.name]
+  onlyIncludeFiles = ['package.json']
+```
+
+_Note: Files included from previous phases will also be available._
+
+### Paths
+
+Paths to append to the `PATH` environment variable.
+
+```toml
+[phase.name]
+  paths = ['/app/node_modules/.bin']
+```
+
+## Start Phase
+
+This configures how a container created from the image will start.
+
+### Command
+
+The command to run when starting the container.
+
+```toml
+[start]
+  cmd = "yarn run start"
+```
+
+### Run image
+
+The runtime image to use. If not specified, the same build image will be used.
+
+```toml
+[start]
+  runImage = 'debian:bullseye-slim'
+```
+
+### Included files
+
+Must be used in combination with `runImage`. The only files that should be copied over to the run image. If no value is specified, the entire app directory is copied over.
+
+```toml
+[start]
+  onlyIncludeFiles = ['./bin/rust-custom-version']
+```
diff --git a/docs/pages/docs/configuration/procfile.md b/docs/pages/docs/configuration/procfile.md
new file mode 100644
index 0000000..96e9883
--- /dev/null
+++ b/docs/pages/docs/configuration/procfile.md
@@ -0,0 +1,22 @@
+---
+title: Procfiles
+---
+
+# {% $markdoc.frontmatter.title %}
+
+The standard Procfile format is supported by Nixpacks. However, only a single process is supported. The command specified in the Procfile will override the provider start command.
+
+```toml
+web: npm run start
+```
+
+## Release process
+
+If a release process is found, a new phase is added that will run this command. The release phase will run after the build.
+
+```toml
+web: npm run start
+
+# Will be run after the build phase
+release: npm run migrate:deploy
+```
diff --git a/docs/sidebar.ts b/docs/sidebar.ts
index daceb15..214f6c0 100644
--- a/docs/sidebar.ts
+++ b/docs/sidebar.ts
@@ -11,7 +11,15 @@ export const sidebarItems: ISidebarSection[] = [
   { href: "/docs/getting-started", text: "Getting Started" },
   { href: "/docs/install", text: "Installation" },
   { href: "/docs/how-it-works", text: "How it Works" },
-  { text: "Configuration", href: "/docs/config" },
+  {
+    text: "Configuration",
+    links: [
+      { text: "Configuration File", href: "/docs/configuration/file" },
+      { text: "Environment", href: "/docs/configuration/environment" },
+      { text: "Procfile", href: "/docs/configuration/procfile" },
+      { text: "Caching", href: "/docs/configuration/caching" },
+    ],
+  },
   { text: "CLI Reference", href: "/docs/cli" },
   {
     text: "Language Support",
@@ -40,7 +48,7 @@ export const sidebarItems: ISidebarSection[] = [
     text: "Deploying",
     links: [
       { text: "Railway", href: "/docs/deploying/railway" },
-      { text: "Easypanel", href: "/docs/deploying/easypanel" }
-     ],
+      { text: "Easypanel", href: "/docs/deploying/easypanel" },
+    ],
   },
 ];
diff --git a/examples/config-json-file/nixpacks.json b/examples/config-json-file/nixpacks.json
new file mode 100644
index 0000000..d634cb2
--- /dev/null
+++ b/examples/config-json-file/nixpacks.json
@@ -0,0 +1,11 @@
+{
+  "phases": {
+    "setup": {
+      "name": "setup",
+      "nixPkgs": ["cowsay"]
+    }
+  },
+  "start": {
+    "cmd": "not the start command"
+  }
+}
diff --git a/examples/custom-plan-path/custom-nixpacks.toml b/examples/custom-plan-path/custom-nixpacks.toml
new file mode 100644
index 0000000..cb3be7a
--- /dev/null
+++ b/examples/custom-plan-path/custom-nixpacks.toml
@@ -0,0 +1,4 @@
+providers = []
+
+[start]
+cmd = "correct start command"
diff --git a/examples/custom-plan-path/nixpacks.toml b/examples/custom-plan-path/nixpacks.toml
new file mode 100644
index 0000000..4db3e7c
--- /dev/null
+++ b/examples/custom-plan-path/nixpacks.toml
@@ -0,0 +1,4 @@
+providers = []
+
+[start]
+cmd = "not the start command"
diff --git a/examples/procfile/Procfile b/examples/procfile/Procfile
index 1da0cd6..6daedfe 100644
--- a/examples/procfile/Procfile
+++ b/examples/procfile/Procfile
@@ -1,1 +1,2 @@
-web: node index.js
+web: echo start from procfile
+release: echo release
diff --git a/examples/procfile/package.json b/examples/procfile/package.json
index d76ba55..91757b8 100644
--- a/examples/procfile/package.json
+++ b/examples/procfile/package.json
@@ -2,7 +2,9 @@
   "name": "procfile",
   "version": "1.0.0",
   "main": "index.js",
-  "scripts": {},
+  "scripts": {
+    "build": "echo build"
+  },
   "dependencies": {},
   "devDependencies": {}
 }
diff --git a/examples/shell-hello/nixpacks.toml b/examples/shell-hello/nixpacks.toml
new file mode 100644
index 0000000..945154e
--- /dev/null
+++ b/examples/shell-hello/nixpacks.toml
@@ -0,0 +1,5 @@
+[phases.setup]
+nixPkgs = ["cowsay"]
+
+[start]
+cmd = "./start.sh"
diff --git a/src/lib.rs b/src/lib.rs
index 51cb855..c880dbe 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,11 +27,10 @@ use crate::nixpacks::{
     environment::Environment,
     logger::Logger,
     nix::pkg::Pkg,
-    plan::{
-        config::GeneratePlanConfig, generator::NixpacksBuildPlanGenerator, BuildPlan, PlanGenerator,
-    },
+    plan::{generator::NixpacksBuildPlanGenerator, BuildPlan, PlanGenerator},
 };
 use anyhow::{bail, Result};
+use nixpacks::plan::generator::GeneratePlanOptions;
 use providers::{
     clojure::ClojureProvider, crystal::CrystalProvider, csharp::CSharpProvider, dart::DartProvider,
     deno::DenoProvider, elixir::ElixirProvider, fsharp::FSharpProvider, go::GolangProvider,
@@ -71,27 +70,21 @@ pub fn get_providers() -> &'static [&'static dyn Provider] {
 pub fn generate_build_plan(
     path: &str,
     envs: Vec<&str>,
-    plan_options: &GeneratePlanConfig,
+    options: &GeneratePlanOptions,
 ) -> Result<BuildPlan> {
     let app = App::new(path)?;
     let environment = Environment::from_envs(envs)?;
 
-    let mut generator = NixpacksBuildPlanGenerator::new(get_providers(), plan_options.clone());
+    let mut generator = NixpacksBuildPlanGenerator::new(get_providers(), options.clone());
     let plan = generator.generate_plan(&app, &environment)?;
 
-    if let Some(ref phase) = plan.start_phase {
-        if phase.cmd.is_none() && !plan_options.no_error_without_start {
-            bail!("No start command could be found")
-        }
-    }
-
     Ok(plan)
 }
 
 pub fn create_docker_image(
     path: &str,
     envs: Vec<&str>,
-    plan_options: &GeneratePlanConfig,
+    plan_options: &GeneratePlanOptions,
     build_options: &DockerBuilderOptions,
 ) -> Result<()> {
     let app = App::new(path)?;
@@ -100,6 +93,12 @@ pub fn create_docker_image(
     let mut generator = NixpacksBuildPlanGenerator::new(get_providers(), plan_options.clone());
     let plan = generator.generate_plan(&app, &environment)?;
 
+    if let Some(ref phase) = plan.start_phase {
+        if phase.cmd.is_none() && !build_options.no_error_without_start {
+            bail!("No start command could be found")
+        }
+    }
+
     let logger = Logger::new();
     let builder = DockerImageBuilder::new(logger, build_options.clone());
     builder.create_image(app.source.to_str().unwrap(), &plan, &environment)?;
diff --git a/src/main.rs b/src/main.rs
index 06cc59e..3b4a1f4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,17 @@
+use anyhow::{bail, Result};
+use clap::{arg, Arg, Command};
+use nixpacks::{
+    create_docker_image, generate_build_plan,
+    nixpacks::{
+        builder::docker::DockerBuilderOptions,
+        nix::pkg::Pkg,
+        plan::{
+            generator::GeneratePlanOptions,
+            phase::{Phase, StartPhase},
+            BuildPlan,
+        },
+    },
+};
 use std::{
     collections::hash_map::DefaultHasher,
     env,
@@ -5,14 +19,20 @@ use std::{
     string::ToString,
 };
 
-use anyhow::Result;
-use clap::{arg, Arg, Command};
-use nixpacks::{
-    create_docker_image, generate_build_plan,
-    nixpacks::{
-        builder::docker::DockerBuilderOptions, nix::pkg::Pkg, plan::config::GeneratePlanConfig,
-    },
-};
+enum PlanFormat {
+    Json,
+    Toml,
+}
+
+impl PlanFormat {
+    fn from_str(s: &str) -> Result<Self> {
+        match s {
+            "json" => Ok(PlanFormat::Json),
+            "toml" => Ok(PlanFormat::Toml),
+            _ => bail!("Invalid plan format"),
+        }
+    }
+}
 
 fn main() -> Result<()> {
     const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -24,7 +44,13 @@ fn main() -> Result<()> {
         .subcommand(
             Command::new("plan")
                 .about("Generate a build plan for an app")
-                .arg(arg!(<PATH> "App source")),
+                .arg(arg!(<PATH> "App source"))
+                .arg(
+                    Arg::new("format")
+                        .short('f')
+                        .takes_value(true)
+                        .help("json|toml. Specify the output format of the plan"),
+                ),
         )
         .subcommand(
             Command::new("build")
@@ -101,6 +127,11 @@ fn main() -> Result<()> {
                     Arg::new("cache-from")
                         .long("cache-from")
                         .help("Image to consider as cache sources"),
+                )
+                .arg(
+                    Arg::new("no-error-without-start")
+                        .long("no-error-without-start")
+                        .help("Do not error when no start command can be found"),
                 ),
         )
         .arg(
@@ -152,13 +183,6 @@ fn main() -> Result<()> {
                 .multiple_values(true)
                 .global(true),
         )
-        .arg(
-            Arg::new("pin")
-                .long("pin")
-                .help("Pin the nixpkgs")
-                .takes_value(false)
-                .global(true),
-        )
         .arg(
             Arg::new("env")
                 .long("env")
@@ -168,9 +192,10 @@ fn main() -> Result<()> {
                 .global(true),
         )
         .arg(
-            Arg::new("no-error-without-start")
-                .long("no-error-without-start")
-                .help("Do not error when no start command can be found")
+            Arg::new("config")
+                .short('c')
+                .help("Path to config file")
+                .takes_value(true)
                 .global(true),
         )
         .get_matches();
@@ -191,33 +216,53 @@ fn main() -> Result<()> {
         None => Vec::new(),
     };
 
-    let pin_pkgs = matches.is_present("pin");
-
     let envs: Vec<_> = match matches.values_of("env") {
         Some(envs) => envs.collect(),
         None => Vec::new(),
     };
-    let no_error_without_start = matches.is_present("no-error-without-start");
 
-    let plan_options = &GeneratePlanConfig {
-        custom_install_cmd: install_cmd,
-        custom_start_cmd: start_cmd,
-        custom_build_cmd: build_cmd,
-        custom_pkgs: pkgs,
-        custom_libs: libs,
-        custom_apt_pkgs: apt_pkgs,
-        pin_pkgs,
-        no_error_without_start,
-        ..Default::default()
+    // CLI build plan
+    let mut cli_plan = BuildPlan::default();
+    if !pkgs.is_empty() || !libs.is_empty() || !apt_pkgs.is_empty() {
+        let mut setup = Phase::setup(Some(vec![pkgs, vec![Pkg::new("...")]].concat()));
+        setup.apt_pkgs = Some(vec![apt_pkgs, vec!["...".to_string()]].concat());
+        setup.nix_libs = Some(vec![libs, vec!["...".to_string()]].concat());
+        cli_plan.add_phase(setup);
+    }
+    if let Some(install_cmds) = install_cmd {
+        let mut install = Phase::install(None);
+        install.cmds = Some(install_cmds);
+        cli_plan.add_phase(install);
+    }
+    if let Some(build_cmds) = build_cmd {
+        let mut build = Phase::build(None);
+        build.cmds = Some(build_cmds);
+        cli_plan.add_phase(build);
+    }
+    if let Some(start_cmd) = start_cmd {
+        let start = StartPhase::new(start_cmd);
+        cli_plan.set_start_phase(start);
+    }
+
+    let config_file = matches.value_of("config").map(ToString::to_string);
+    let options = GeneratePlanOptions {
+        plan: Some(cli_plan),
+        config_file,
     };
 
     match &matches.subcommand() {
         Some(("plan", matches)) => {
             let path = matches.value_of("PATH").expect("required");
+            let format = PlanFormat::from_str(matches.value_of("format").unwrap_or("json"))?;
 
-            let plan = generate_build_plan(path, envs, plan_options)?;
-            let json = serde_json::to_string_pretty(&plan)?;
-            println!("{}", json);
+            let plan = generate_build_plan(path, envs, &options)?;
+
+            let plan_s = match format {
+                PlanFormat::Json => serde_json::to_string_pretty(&plan)?,
+                PlanFormat::Toml => toml::to_string_pretty(&plan)?,
+            };
+
+            println!("{}", plan_s);
         }
         Some(("build", matches)) => {
             let path = matches.value_of("PATH").expect("required");
@@ -250,6 +295,8 @@ fn main() -> Result<()> {
                 .map(|values| values.map(ToString::to_string).collect::<Vec<_>>())
                 .unwrap_or_default();
 
+            let no_error_without_start = matches.is_present("no-error-without-start");
+
             let build_options = &DockerBuilderOptions {
                 name,
                 tags,
@@ -263,9 +310,10 @@ fn main() -> Result<()> {
                 current_dir,
                 inline_cache,
                 cache_from,
+                no_error_without_start,
             };
 
-            create_docker_image(path, envs, plan_options, build_options)?;
+            create_docker_image(path, envs, &options, build_options)?;
         }
         _ => eprintln!("Invalid command"),
     }
diff --git a/src/nixpacks/app.rs b/src/nixpacks/app.rs
index 72ec2d9..6ae13bb 100644
--- a/src/nixpacks/app.rs
+++ b/src/nixpacks/app.rs
@@ -1,12 +1,11 @@
-use path_slash::PathBufExt;
-use std::collections::BTreeMap;
-use std::path::Path;
-use std::{env, fs, path::PathBuf};
-
 use anyhow::{bail, Context, Result};
 use globset::Glob;
+use path_slash::PathBufExt;
 use regex::Regex;
 use serde::de::DeserializeOwned;
+use std::collections::BTreeMap;
+use std::path::Path;
+use std::{env, fs, path::PathBuf};
 use walkdir::{DirEntry, WalkDir};
 
 pub type StaticAssets = BTreeMap<String, String>;
diff --git a/src/nixpacks/builder/docker/dockerfile_generation.rs b/src/nixpacks/builder/docker/dockerfile_generation.rs
index d31c16c..7eed5ec 100644
--- a/src/nixpacks/builder/docker/dockerfile_generation.rs
+++ b/src/nixpacks/builder/docker/dockerfile_generation.rs
@@ -2,6 +2,7 @@ use super::{utils, DockerBuilderOptions};
 use crate::nixpacks::{
     app,
     environment::Environment,
+    images::DEFAULT_BASE_IMAGE,
     nix,
     plan::{
         phase::{Phase, StartPhase},
@@ -132,15 +133,21 @@ impl DockerfileGenerator for BuildPlan {
             format!("COPY {} {}", rel_assets_slash_path, app::ASSETS_DIR)
         };
 
-        let dockerfile_phases = plan
-            .get_sorted_phases()?
+        let phases = plan.get_sorted_phases()?;
+
+        let dockerfile_phases = phases
+            .clone()
             .into_iter()
             .map(|phase| {
-                let phase_dockerfile = phase
-                    .generate_dockerfile(options, env, output)
-                    .context(format!("Generating Dockerfile for phase {}", phase.name))?;
-
-                match phase.name.as_str() {
+                let phase_dockerfile =
+                    phase
+                        .generate_dockerfile(options, env, output)
+                        .context(format!(
+                            "Generating Dockerfile for phase {}",
+                            phase.get_name()
+                        ))?;
+
+                match phase.get_name().as_str() {
                     // We want to load the variables immediately after the setup phase
                     "setup" => Ok(format!(
                         "{}\n# load variables\n{}\n",
@@ -152,13 +159,29 @@ impl DockerfileGenerator for BuildPlan {
             .collect::<Result<Vec<_>>>();
         let dockerfile_phases_str = dockerfile_phases?.join("\n");
 
+        // Handle the case where there is only a setup phase and all the app files have not been copied into
+        // the image yet
+        let setup_copy_command = match (phases.first(), phases.len()) {
+            (Some(first_phase), 1) => {
+                if first_phase.get_name() == "setup" {
+                    utils::get_copy_command(&[".".to_string()], APP_DIR)
+                } else {
+                    "".to_string()
+                }
+            }
+            _ => "".to_string(),
+        };
+
         let start_phase_str = plan
             .start_phase
             .clone()
             .unwrap_or_default()
             .generate_dockerfile(options, env, output)?;
 
-        let base_image = plan.build_image.clone();
+        let base_image = plan
+            .build_image
+            .clone()
+            .unwrap_or_else(|| DEFAULT_BASE_IMAGE.to_string());
 
         let dockerfile = formatdoc! {"
             FROM {base_image}
@@ -170,12 +193,14 @@ impl DockerfileGenerator for BuildPlan {
 
             {dockerfile_phases_str}
 
+            {setup_copy_command}
             {start_phase_str}
         ", 
         base_image=base_image,
         APP_DIR=APP_DIR,
         assets_copy_cmd=assets_copy_cmd,
         dockerfile_phases_str=dockerfile_phases_str,
+        setup_copy_command=setup_copy_command,
         start_phase_str=start_phase_str};
 
         Ok(dockerfile)
@@ -192,7 +217,7 @@ impl DockerfileGenerator for BuildPlan {
         for phase in self.get_sorted_phases()? {
             phase
                 .write_supporting_files(options, env, output)
-                .context(format!("Writing files for phase {}", phase.name))?;
+                .context(format!("Writing files for phase {}", phase.get_name()))?;
         }
 
         Ok(())
@@ -302,7 +327,7 @@ impl DockerfileGenerator for Phase {
 
         // Install nix packages and libraries
         let install_nix_pkgs_str = if self.uses_nix() {
-            let nix_file = output.get_relative_path(format!("{}.nix", phase.name));
+            let nix_file = output.get_relative_path(format!("{}.nix", phase.get_name()));
 
             let nix_file_path = nix_file
                 .to_slash()
@@ -327,7 +352,7 @@ impl DockerfileGenerator for Phase {
         };
 
         // Copy over app files
-        let phase_files = match (phase.name.as_str(), &phase.only_include_files) {
+        let phase_files = match (phase.get_name().as_str(), &phase.only_include_files) {
             (_, Some(files)) => files.clone(),
             // Special case for the setup phase, which has no files
             ("setup", None) => vec![],
@@ -362,7 +387,7 @@ impl DockerfileGenerator for Phase {
             # {name} phase
             {dockerfile_stmts}
         ", 
-          name=phase.name,
+          name=phase.get_name(),
           dockerfile_stmts=dockerfile_stmts
         };
 
@@ -377,7 +402,7 @@ impl DockerfileGenerator for Phase {
     ) -> Result<()> {
         if self.uses_nix() {
             // Write the Nix expressions to the output directory
-            let nix_file_name = format!("{}.nix", self.name);
+            let nix_file_name = format!("{}.nix", self.get_name());
             let nix_path = output.get_absolute_path(nix_file_name);
             let nix_expression = nix::create_nix_expression(self);
 
diff --git a/src/nixpacks/builder/docker/mod.rs b/src/nixpacks/builder/docker/mod.rs
index 5675f3c..ae0334a 100644
--- a/src/nixpacks/builder/docker/mod.rs
+++ b/src/nixpacks/builder/docker/mod.rs
@@ -15,6 +15,7 @@ pub struct DockerBuilderOptions {
     pub cache_from: Option<String>,
     pub platform: Vec<String>,
     pub current_dir: bool,
+    pub no_error_without_start: bool,
 }
 
 mod cache;
diff --git a/src/nixpacks/nix/mod.rs b/src/nixpacks/nix/mod.rs
index 09408aa..07ed861 100644
--- a/src/nixpacks/nix/mod.rs
+++ b/src/nixpacks/nix/mod.rs
@@ -5,17 +5,11 @@ use super::plan::phase::Phase;
 pub mod pkg;
 
 pub fn create_nix_expression(phase: &Phase) -> String {
-    let pkgs = phase.nix_pkgs.clone().unwrap_or_default();
+    let nixpkgs = phase.nix_pkgs.clone().unwrap_or_default().join(" ");
 
-    let nixpkgs = pkgs
-        .iter()
-        .map(pkg::Pkg::to_nix_string)
-        .collect::<Vec<String>>()
-        .join(" ");
-
-    let libraries = phase.nix_libraries.clone().unwrap_or_default().join(" ");
+    let libraries = phase.nix_libs.clone().unwrap_or_default().join(" ");
 
-    let nix_archive = phase.nixpacks_archive.clone();
+    let nix_archive = phase.nixpkgs_archive.clone();
     let pkg_import = match nix_archive {
         Some(archive) => format!(
             "import (fetchTarball \"https://github.com/NixOS/nixpkgs/archive/{}.tar.gz\")",
@@ -24,12 +18,7 @@ pub fn create_nix_expression(phase: &Phase) -> String {
         None => "import <nixpkgs>".to_string(),
     };
 
-    let mut overlays: Vec<String> = Vec::new();
-    for pkg in &pkgs {
-        if let Some(overlay) = &pkg.overlay {
-            overlays.push(overlay.to_string());
-        }
-    }
+    let overlays = phase.nix_overlays.clone().unwrap_or_default();
 
     let overlays_string = overlays
         .iter()
@@ -48,7 +37,7 @@ pub fn create_nix_expression(phase: &Phase) -> String {
         String::new()
     };
 
-    let name = format!("{}-env", phase.name);
+    let name = format!("{}-env", phase.get_name());
     let nix_expression = formatdoc! {"
             {{ }}:
 
diff --git a/src/nixpacks/plan/config.rs b/src/nixpacks/plan/config.rs
deleted file mode 100644
index 0e520d0..0000000
--- a/src/nixpacks/plan/config.rs
+++ /dev/null
@@ -1,197 +0,0 @@
-use crate::nixpacks::{environment::Environment, nix::pkg::Pkg};
-
-#[derive(Eq, PartialEq, Clone, Default, Debug)]
-pub struct GeneratePlanConfig {
-    pub custom_install_cmd: Option<Vec<String>>,
-    pub custom_build_cmd: Option<Vec<String>>,
-    pub custom_start_cmd: Option<String>,
-    pub custom_pkgs: Vec<Pkg>,
-    pub custom_libs: Vec<String>,
-    pub custom_apt_pkgs: Vec<String>,
-    pub pin_pkgs: bool,
-    pub install_cache_dirs: Option<Vec<String>>,
-    pub build_cache_dirs: Option<Vec<String>>,
-    pub no_error_without_start: bool,
-}
-
-impl GeneratePlanConfig {
-    pub fn new() -> Self {
-        Self {
-            ..Default::default()
-        }
-    }
-
-    /// Create configuration from the given environment variables.
-    pub fn from_environment(env: &Environment) -> Self {
-        Self {
-            custom_install_cmd: env.get_config_variable("INSTALL_CMD").map(|cmd| vec![cmd]),
-            custom_build_cmd: env.get_config_variable("BUILD_CMD").map(|cmd| vec![cmd]),
-            custom_start_cmd: env.get_config_variable("START_CMD"),
-            custom_pkgs: env
-                .get_config_variable("PKGS")
-                .map(|pkg_string| pkg_string.split(' ').map(Pkg::new).collect::<Vec<_>>())
-                .unwrap_or_default(),
-            custom_apt_pkgs: env
-                .get_config_variable("APT_PKGS")
-                .map(|apt_string| apt_string.split(' ').map(String::from).collect::<Vec<_>>())
-                .unwrap_or_default(),
-            custom_libs: env
-                .get_config_variable("LIBS")
-                .map(|lib_string| lib_string.split(' ').map(String::from).collect::<Vec<_>>())
-                .unwrap_or_default(),
-            install_cache_dirs: env
-                .get_config_variable("INSTALL_CACHE_DIRS")
-                .map(|dirs| dirs.split(' ').map(String::from).collect::<Vec<_>>()),
-            build_cache_dirs: env
-                .get_config_variable("BUILD_CACHE_DIRS")
-                .map(|dirs| dirs.split(' ').map(String::from).collect::<Vec<_>>()),
-            ..Default::default()
-        }
-    }
-
-    /// Merge two configurations, preferring the values from the second.
-    pub fn merge(c1: &Self, c2: &Self) -> Self {
-        Self {
-            custom_install_cmd: c1
-                .custom_install_cmd
-                .clone()
-                .or_else(|| c2.custom_install_cmd.clone()),
-            custom_build_cmd: c1
-                .custom_build_cmd
-                .clone()
-                .or_else(|| c2.custom_build_cmd.clone()),
-            custom_start_cmd: c1
-                .custom_start_cmd
-                .clone()
-                .or_else(|| c2.custom_start_cmd.clone()),
-            custom_pkgs: c1
-                .custom_pkgs
-                .iter()
-                .chain(c2.custom_pkgs.iter())
-                .cloned()
-                .collect(),
-            custom_libs: c1
-                .custom_libs
-                .iter()
-                .chain(c2.custom_libs.iter())
-                .cloned()
-                .collect(),
-            custom_apt_pkgs: c1
-                .custom_apt_pkgs
-                .iter()
-                .chain(c2.custom_apt_pkgs.iter())
-                .cloned()
-                .collect(),
-            pin_pkgs: c1.pin_pkgs || c2.pin_pkgs,
-            install_cache_dirs: combine_option_vec(
-                c1.install_cache_dirs.clone(),
-                c2.install_cache_dirs.clone(),
-            ),
-            build_cache_dirs: combine_option_vec(
-                c1.build_cache_dirs.clone(),
-                c2.build_cache_dirs.clone(),
-            ),
-            no_error_without_start: c1.no_error_without_start || c2.no_error_without_start,
-        }
-    }
-}
-
-fn combine_option_vec<T: Clone>(v1: Option<Vec<T>>, v2: Option<Vec<T>>) -> Option<Vec<T>> {
-    match (v1, v2) {
-        (Some(v1), Some(v2)) => Some(v1.iter().chain(v2.iter()).cloned().collect()),
-        (Some(v1), None) => Some(v1),
-        (None, Some(v2)) => Some(v2),
-        (None, None) => None,
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_combine_option_vec() {
-        assert_eq!(Some(vec!["a"]), combine_option_vec(Some(vec!["a"]), None));
-        assert_eq!(Some(vec!["b"]), combine_option_vec(None, Some(vec!["b"])));
-        assert_eq!(
-            Some(vec!["a", "b"]),
-            combine_option_vec(Some(vec!["a"]), Some(vec!["b"]))
-        );
-    }
-
-    #[test]
-    fn test_config_from_environment_variables() {
-        assert_eq!(
-            GeneratePlanConfig::default(),
-            GeneratePlanConfig::from_environment(&Environment::from_envs(Vec::new()).unwrap())
-        );
-        assert_eq!(
-            GeneratePlanConfig {
-                custom_install_cmd: Some(vec!["install".to_string()]),
-                custom_build_cmd: Some(vec!["build".to_string()]),
-                custom_start_cmd: Some("start".to_string()),
-                custom_pkgs: vec![Pkg::new("cowsay")],
-                custom_apt_pkgs: vec!["wget".to_string()],
-                custom_libs: vec!["openssl".to_string()],
-                install_cache_dirs: Some(vec!["install/cache".to_string()]),
-                build_cache_dirs: Some(vec!["build/cache".to_string()]),
-                pin_pkgs: false,
-                no_error_without_start: false
-            },
-            GeneratePlanConfig::from_environment(
-                &Environment::from_envs(vec![
-                    "NIXPACKS_INSTALL_CMD=install",
-                    "NIXPACKS_BUILD_CMD=build",
-                    "NIXPACKS_START_CMD=start",
-                    "NIXPACKS_PKGS=cowsay",
-                    "NIXPACKS_APT_PKGS=wget",
-                    "NIXPACKS_LIBS=openssl",
-                    "NIXPACKS_INSTALL_CACHE_DIRS=install/cache",
-                    "NIXPACKS_BUILD_CACHE_DIRS=build/cache",
-                ])
-                .unwrap()
-            )
-        );
-    }
-
-    #[test]
-    fn test_config_merge() {
-        assert_eq!(
-            GeneratePlanConfig {
-                custom_install_cmd: Some(vec!["install".to_string()]),
-                custom_build_cmd: Some(vec!["build".to_string()]),
-                custom_start_cmd: Some("start".to_string()),
-                custom_pkgs: vec![Pkg::new("pkg1"), Pkg::new("pkg2")],
-                custom_apt_pkgs: vec!["curl".to_string(), "wget".to_string()],
-                custom_libs: vec!["openssl".to_string()],
-                install_cache_dirs: Some(vec!["install/cache".to_string(), "install2".to_string()]),
-                build_cache_dirs: Some(vec!["build/cache".to_string()]),
-                pin_pkgs: false,
-                no_error_without_start: false
-            },
-            GeneratePlanConfig::merge(
-                &GeneratePlanConfig::from_environment(
-                    &Environment::from_envs(vec![
-                        "NIXPACKS_INSTALL_CMD=install",
-                        "NIXPACKS_START_CMD=start",
-                        "NIXPACKS_PKGS=pkg1",
-                        "NIXPACKS_APT_PKGS=curl",
-                        "NIXPACKS_INSTALL_CACHE_DIRS=install/cache install2",
-                    ])
-                    .unwrap()
-                ),
-                &GeneratePlanConfig::from_environment(
-                    &Environment::from_envs(vec![
-                        "NIXPACKS_BUILD_CMD=build",
-                        "NIXPACKS_START_CMD=start",
-                        "NIXPACKS_PKGS=pkg2",
-                        "NIXPACKS_APT_PKGS=wget",
-                        "NIXPACKS_LIBS=openssl",
-                        "NIXPACKS_BUILD_CACHE_DIRS=build/cache",
-                    ])
-                    .unwrap()
-                )
-            )
-        );
-    }
-}
diff --git a/src/nixpacks/plan/generator.rs b/src/nixpacks/plan/generator.rs
index d568f4d..dbafb92 100644
--- a/src/nixpacks/plan/generator.rs
+++ b/src/nixpacks/plan/generator.rs
@@ -1,14 +1,17 @@
-use super::{config::GeneratePlanConfig, BuildPlan, PlanGenerator};
+use std::path::Path;
+
 use crate::{
     nixpacks::{
         app::App,
         environment::{Environment, EnvironmentVariables},
-        nix::pkg::Pkg,
+        plan::{BuildPlan, PlanGenerator},
     },
-    providers::Provider,
+    providers::{procfile::ProcfileProvider, Provider},
 };
-use anyhow::{Context, Ok, Result};
-use std::collections::HashMap;
+use anyhow::{bail, Context, Ok, Result};
+use colored::Colorize;
+
+use super::merge::Mergeable;
 
 // This line is automatically updated.
 // Last Modified: 2022-09-05 17:08:13 UTC+0000
@@ -18,27 +21,17 @@ const NIXPACKS_METADATA: &str = "NIXPACKS_METADATA";
 
 #[derive(Clone, Default, Debug)]
 pub struct GeneratePlanOptions {
-    pub custom_install_cmd: Option<Vec<String>>,
-    pub custom_build_cmd: Option<Vec<String>>,
-    pub custom_start_cmd: Option<String>,
-    pub custom_pkgs: Vec<Pkg>,
-    pub custom_libs: Vec<String>,
-    pub custom_apt_pkgs: Vec<String>,
-    pub pin_pkgs: bool,
-    pub plan_path: Option<String>,
+    pub plan: Option<BuildPlan>,
+    pub config_file: Option<String>,
 }
 
 pub struct NixpacksBuildPlanGenerator<'a> {
     providers: &'a [&'a dyn Provider],
-    matched_provider: Option<&'a dyn Provider>,
-    config: GeneratePlanConfig,
+    config: GeneratePlanOptions,
 }
 
 impl<'a> PlanGenerator for NixpacksBuildPlanGenerator<'a> {
     fn generate_plan(&mut self, app: &App, environment: &Environment) -> Result<BuildPlan> {
-        // Match a specific provider
-        self.detect(app, environment)?;
-
         // If the provider defines a build plan in the new format, use that
         let plan = self.get_build_plan(app, environment)?;
 
@@ -49,112 +42,148 @@ impl<'a> PlanGenerator for NixpacksBuildPlanGenerator<'a> {
 impl NixpacksBuildPlanGenerator<'_> {
     pub fn new<'a>(
         providers: &'a [&'a dyn Provider],
-        config: GeneratePlanConfig,
+        config: GeneratePlanOptions,
     ) -> NixpacksBuildPlanGenerator<'a> {
-        NixpacksBuildPlanGenerator {
-            providers,
-            matched_provider: None,
-            config,
-        }
+        NixpacksBuildPlanGenerator { providers, config }
     }
 
-    /// Match a single provider from the given app and environment.
-    fn detect(&mut self, app: &App, environment: &Environment) -> Result<()> {
-        for &provider in self.providers {
-            let matches = provider.detect(app, environment)?;
-            if matches {
-                self.matched_provider = Some(provider);
-                break;
-            }
+    /// Get a build plan from the provider and by applying the config from the environment
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<BuildPlan> {
+        let file_plan = self.read_file_plan(app)?;
+        let env_plan = BuildPlan::from_environment(env);
+        let cli_plan = self.config.plan.clone().unwrap_or_default();
+        let plan_before_providers = BuildPlan::merge_plans(&vec![file_plan, env_plan, cli_plan]);
+
+        let provider_plan =
+            self.get_plan_from_providers(plan_before_providers.providers.clone(), app, env)?;
+
+        let procfile_plan = (ProcfileProvider {})
+            .get_build_plan(app, env)?
+            .unwrap_or_default();
+
+        let mut plan =
+            BuildPlan::merge_plans(&vec![provider_plan, procfile_plan, plan_before_providers]);
+
+        if !env.get_variable_names().is_empty() {
+            plan.add_variables(Environment::clone_variables(env));
         }
 
-        Ok(())
+        plan.pin();
+
+        Ok(plan)
     }
 
-    /// Get a build plan from the provider and by applying the config from the environment
-    fn get_build_plan(&self, app: &App, environment: &Environment) -> Result<BuildPlan> {
-        // Merge the config from the CLI flags with the config from the environment variables
-        // The CLI config takes precedence
-        let config = GeneratePlanConfig::merge(
-            &GeneratePlanConfig::from_environment(environment),
-            &self.config,
-        );
+    fn get_auto_providers(&self, app: &App, env: &Environment) -> Result<Vec<String>> {
+        let mut providers = Vec::new();
 
-        let mut plan = BuildPlan::default();
+        for provider in self.providers {
+            if provider.detect(app, env)? {
+                providers.push(provider.name().to_string());
 
-        if let Some(provider) = self.matched_provider {
-            if let Some(provider_build_plan) = provider.get_build_plan(app, environment)? {
-                plan = provider_build_plan;
-                plan.add_variables(self.get_nixpacks_env_vars(provider, app, environment)?);
+                // Only match a single provider... for now
+                break;
             }
         }
 
-        // The Procfile start command has precedence over the provider's start command
-        if let Some(procfile_start) = self.get_procfile_start_cmd(app)? {
-            let mut start_phase = plan.start_phase.clone().unwrap_or_default();
-            start_phase.cmd = Some(procfile_start);
-            plan.set_start_phase(start_phase);
+        Ok(providers)
+    }
+
+    fn get_plan_from_providers(
+        &self,
+        provider_names: Option<Vec<String>>,
+        app: &App,
+        env: &Environment,
+    ) -> Result<BuildPlan> {
+        let provider_names = if let Some(provider_names) = provider_names {
+            provider_names
+        } else {
+            self.get_auto_providers(app, env)?
+        };
+
+        if provider_names.len() > 1 {
+            bail!("Only a single provider is supported at this time");
         }
 
-        // The Procfiles release command is append to the provider's build command
-        if let Some(procfile_release) = self.get_procfile_release_cmd(app)? {
-            if let Some(build) = plan.get_phase_mut("build") {
-                build.add_cmd(procfile_release);
+        let mut plan = BuildPlan::default();
+        let mut count = 0;
+
+        let mut metadata = Vec::new();
+
+        for name in provider_names {
+            let provider = self.providers.iter().find(|p| p.name() == name);
+            if let Some(provider) = provider {
+                if let Some(mut provider_plan) = provider.get_build_plan(app, env)? {
+                    // All but the first provider have their phases prefixed with their name
+                    if count > 0 {
+                        provider_plan.prefix_phases(provider.name());
+                    }
+
+                    let metadata_string = provider
+                        .metadata(app, env)?
+                        .join_as_comma_separated(provider.name().to_owned());
+                    metadata.push(metadata_string);
+
+                    plan = BuildPlan::merge(&plan, &provider_plan);
+                }
+            } else {
+                bail!("Provider {} not found", name);
             }
-        }
 
-        plan = BuildPlan::apply_config(&plan, &config);
+            count += 1;
+        }
 
-        if !environment.get_variable_names().is_empty() {
-            plan.add_variables(Environment::clone_variables(environment));
+        if count > 0 {
+            plan.add_variables(EnvironmentVariables::from([(
+                NIXPACKS_METADATA.to_string(),
+                metadata.join(","),
+            )]));
         }
 
         Ok(plan)
     }
 
-    fn get_procfile_start_cmd(&self, app: &App) -> Result<Option<String>> {
-        if app.includes_file("Procfile") {
-            let mut procfile: HashMap<String, String> =
-                app.read_yaml("Procfile").context("Reading Procfile")?;
-            procfile.remove("release");
-            if procfile.is_empty() {
-                Ok(None)
-            } else {
-                let process = procfile.values().collect::<Vec<_>>()[0].to_string();
-                Ok(Some(process))
-            }
+    fn read_file_plan(&self, app: &App) -> Result<BuildPlan> {
+        let file_path = if let Some(file_path) = &self.config.config_file {
+            Some(file_path.clone())
+        } else if app.includes_file("nixpacks.toml") {
+            Some("nixpacks.toml".to_owned())
+        } else if app.includes_file("nixpacks.json") {
+            Some("nixpacks.json".to_owned())
         } else {
-            Ok(None)
-        }
-    }
-
-    fn get_procfile_release_cmd(&self, app: &App) -> Result<Option<String>> {
-        if app.includes_file("Procfile") {
-            let procfile: HashMap<String, String> =
-                app.read_yaml("Procfile").context("Reading Procfile")?;
-            if let Some(release) = procfile.get("release") {
-                Ok(Some(release.to_string()))
+            None
+        };
+
+        let plan =
+            if let Some(file_path) = file_path {
+                let filename = Path::new(&file_path);
+                let ext = filename.extension().unwrap_or_default();
+
+                let contents = app.read_file(file_path.as_str()).with_context(|| {
+                    format!("Failed to read Nixpacks config file `{}`", file_path)
+                })?;
+                let plan = if ext == "toml" {
+                    BuildPlan::from_toml(&contents)
+                } else if ext == "json" {
+                    BuildPlan::from_json(&contents)
+                } else {
+                    bail!("Unknown file type: {}", file_path)
+                };
+
+                Some(plan.with_context(|| {
+                    format!("Failed to parse Nixpacks config file `{}`", file_path)
+                })?)
             } else {
-                Ok(None)
-            }
-        } else {
-            Ok(None)
+                None
+            };
+
+        if plan.is_some() {
+            println!(
+                "{}",
+                "\n Nixpacks file based configuration is experimental and may change\n"
+                    .bright_yellow()
+            );
         }
-    }
 
-    fn get_nixpacks_env_vars(
-        &self,
-        provider: &dyn Provider,
-        app: &App,
-        env: &Environment,
-    ) -> Result<EnvironmentVariables> {
-        let metadata_string = provider
-            .metadata(app, env)?
-            .join_as_comma_separated(provider.name().to_owned());
-
-        Ok(EnvironmentVariables::from([(
-            NIXPACKS_METADATA.to_string(),
-            metadata_string,
-        )]))
+        Ok(plan.unwrap_or_default())
     }
 }
diff --git a/src/nixpacks/plan/merge.rs b/src/nixpacks/plan/merge.rs
new file mode 100644
index 0000000..0ce8bdd
--- /dev/null
+++ b/src/nixpacks/plan/merge.rs
@@ -0,0 +1,267 @@
+use super::{
+    phase::{Phase, StartPhase},
+    BuildPlan,
+};
+
+pub trait Mergeable {
+    fn merge(c1: &Self, c2: &Self) -> Self;
+}
+
+impl Mergeable for BuildPlan {
+    fn merge(c1: &BuildPlan, c2: &BuildPlan) -> BuildPlan {
+        let mut new_plan = c1.clone();
+        let plan2 = c2.clone();
+
+        new_plan.providers = fill_auto_in_vec(new_plan.providers.clone(), plan2.providers.clone());
+        new_plan.build_image = new_plan.build_image.or(plan2.build_image);
+
+        new_plan.static_assets = match (new_plan.static_assets, plan2.static_assets) {
+            (None, assets) | (assets, None) => assets,
+            (Some(assets1), Some(assets2)) => {
+                let mut assets = assets1;
+                assets.extend(assets2);
+                Some(assets)
+            }
+        };
+
+        new_plan.variables = match (new_plan.variables, plan2.variables) {
+            (None, vars) | (vars, None) => vars,
+            (Some(vars1), Some(vars2)) => {
+                let mut vars = vars1;
+                vars.extend(vars2);
+                Some(vars)
+            }
+        };
+
+        if new_plan.phases.is_none() {
+            new_plan.phases = plan2.phases;
+        } else {
+            for (name, c2_phase) in plan2.phases.clone().unwrap_or_default() {
+                let phase = new_plan.remove_phase(&name);
+                let phase = phase.unwrap_or_else(|| {
+                    let mut phase = Phase::new(name.clone());
+                    if name == "install" {
+                        phase.depends_on_phase("setup");
+                    } else if name == "build" {
+                        phase.depends_on_phase("install");
+                    };
+
+                    phase
+                });
+
+                let merged_phase = Phase::merge(&phase, &c2_phase);
+                new_plan.add_phase(merged_phase);
+            }
+        };
+
+        new_plan.start_phase = match (new_plan.start_phase, plan2.start_phase) {
+            (None, s) | (s, None) => s,
+            (Some(s1), Some(s2)) => Some(StartPhase::merge(&s1, &s2)),
+        };
+
+        new_plan.resolve_phase_names();
+        new_plan
+    }
+}
+
+impl Mergeable for Phase {
+    fn merge(c1: &Phase, c2: &Phase) -> Phase {
+        let mut phase = c1.clone();
+        let c2 = c2.clone();
+        phase.nixpkgs_archive = c2.nixpkgs_archive.or_else(|| phase.nixpkgs_archive.clone());
+
+        phase.cmds = fill_auto_in_vec(phase.cmds.clone(), c2.cmds);
+        phase.depends_on = fill_auto_in_vec(phase.depends_on.clone(), c2.depends_on);
+        phase.nix_pkgs = fill_auto_in_vec(phase.nix_pkgs.clone(), c2.nix_pkgs);
+        phase.nix_libs = fill_auto_in_vec(phase.nix_libs.clone(), c2.nix_libs);
+        phase.apt_pkgs = fill_auto_in_vec(phase.apt_pkgs.clone(), c2.apt_pkgs);
+        phase.nix_overlays = fill_auto_in_vec(phase.nix_overlays.clone(), c2.nix_overlays);
+        phase.only_include_files =
+            fill_auto_in_vec(phase.only_include_files.clone(), c2.only_include_files);
+        phase.cache_directories =
+            fill_auto_in_vec(phase.cache_directories.clone(), c2.cache_directories);
+        phase.paths = fill_auto_in_vec(phase.paths.clone(), c2.paths);
+
+        phase
+    }
+}
+
+impl Mergeable for StartPhase {
+    fn merge(c1: &StartPhase, c2: &StartPhase) -> StartPhase {
+        let mut start_phase = c1.clone();
+        let c2 = c2.clone();
+        start_phase.cmd = c2.cmd.or_else(|| start_phase.cmd.clone());
+        start_phase.run_image = c2.run_image.or_else(|| start_phase.run_image.clone());
+        start_phase.only_include_files = fill_auto_in_vec(
+            start_phase.only_include_files.clone(),
+            c2.only_include_files,
+        );
+        start_phase
+    }
+}
+
+/// Fills in the `"..."`'s or `"@auto"`'s in `replacer` with the values from the `original`
+///
+/// ```
+/// let arr = fill_auto_in_vec(
+///   Some(vec!["a", "b", "c"]),
+///   Some(vec!["x", "...", "z"])
+/// );
+/// assert_eq!(Some(vec!["x", "...", "a", "b", "c", "z"]), arr);
+/// ```
+fn fill_auto_in_vec(
+    original: Option<Vec<String>>,
+    replacer: Option<Vec<String>>,
+) -> Option<Vec<String>> {
+    if let Some(replacer) = replacer {
+        let original = original.unwrap_or_default();
+        let modified = replacer
+            .into_iter()
+            .flat_map(|x| {
+                let v = x.clone();
+                if v == *"@auto" || v == *"..." {
+                    let mut fill = vec![v];
+                    fill.append(&mut original.clone());
+                    fill
+                } else {
+                    vec![x]
+                }
+            })
+            .collect::<Vec<_>>();
+
+        Some(modified)
+    } else {
+        original
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    fn vs(v: Vec<&str>) -> Vec<String> {
+        v.into_iter()
+            .map(std::string::ToString::to_string)
+            .collect()
+    }
+
+    #[test]
+    fn test_merge_plan() {
+        let merged = BuildPlan::merge(
+            &BuildPlan::from_toml(
+                r#"
+                [phases.setup]
+                nixPkgs = ["cowsay"]
+                "#,
+            )
+            .unwrap(),
+            &BuildPlan::from_toml("").unwrap(),
+        );
+
+        assert_eq!(
+            BuildPlan::from_toml(
+                r#"
+                [phases.setup]
+                nixPkgs = ["cowsay"]
+                "#,
+            )
+            .unwrap(),
+            merged
+        );
+
+        let merged = BuildPlan::merge(
+            &BuildPlan::from_toml(
+                r#"
+                [phases.setup]
+                nixPkgs = ["nodejs", "yarn"]
+
+                [phases.build]
+                cmds = ["yarn run build"]
+
+                [start]
+                cmd = "yarn run start"
+                "#,
+            )
+            .unwrap(),
+            &BuildPlan::from_toml(
+                r#"
+                [phases.setup]
+                nixPkgs = ["...", "cowsay"]
+
+                [start]
+                cmd = "yarn run client:start"
+                "#,
+            )
+            .unwrap(),
+        );
+
+        assert_eq!(
+            BuildPlan::from_toml(
+                r#"
+                [phases.setup]
+                nixPkgs = ["...", "nodejs", "yarn", "cowsay"]
+
+                [phases.build]
+                cmds = ["yarn run build"]
+
+                [start]
+                cmd = "yarn run client:start"
+                "#,
+            )
+            .unwrap(),
+            merged
+        );
+
+        let merged = BuildPlan::merge(
+            &BuildPlan::from_toml(
+                r#"
+                [phases.setup]
+                nixPkgs = ["nodejs", "yarn"]
+                "#,
+            )
+            .unwrap(),
+            &BuildPlan::from_toml(
+                r#"
+                providers = []
+
+                [phases.setup]
+                nixPkgs = ["cowsay"]
+                "#,
+            )
+            .unwrap(),
+        );
+
+        assert_eq!(
+            BuildPlan::from_toml(
+                r#"
+                providers = []
+
+                [phases.setup]
+                nixPkgs = ["cowsay"]
+                "#,
+            )
+            .unwrap(),
+            merged
+        );
+    }
+
+    #[test]
+    fn test_fill_auto_in_vec() {
+        assert_eq!(
+            vec!["x", "...", "z"],
+            fill_auto_in_vec(None, Some(vs(vec!["x", "...", "z"]))).unwrap()
+        );
+        assert_eq!(
+            vec!["a", "b", "c"],
+            fill_auto_in_vec(Some(vs(vec!["a", "b", "c"])), None).unwrap()
+        );
+        assert_eq!(
+            vec!["x", "...", "a", "b", "c", "z"],
+            fill_auto_in_vec(
+                Some(vs(vec!["a", "b", "c"])),
+                Some(vs(vec!["x", "...", "z"]))
+            )
+            .unwrap()
+        );
+    }
+}
diff --git a/src/nixpacks/plan/mod.rs b/src/nixpacks/plan/mod.rs
index b0ee54b..9ca3080 100644
--- a/src/nixpacks/plan/mod.rs
+++ b/src/nixpacks/plan/mod.rs
@@ -1,21 +1,20 @@
 use self::{
-    config::GeneratePlanConfig,
-    generator::NIXPKGS_ARCHIVE,
-    phase::{Phase, StartPhase},
+    merge::Mergeable,
+    phase::{Phase, Phases, StartPhase},
     topological_sort::topological_sort,
 };
-use super::{images::DEFAULT_BASE_IMAGE, NIX_PACKS_VERSION};
+use super::images::{DEBIAN_SLIM_IMAGE, DEFAULT_BASE_IMAGE};
 use crate::nixpacks::{
     app::{App, StaticAssets},
     environment::{Environment, EnvironmentVariables},
 };
-
 use anyhow::Result;
-
 use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
 
-pub mod config;
+// pub mod config;
 pub mod generator;
+pub mod merge;
 pub mod phase;
 pub mod pretty_print;
 mod topological_sort;
@@ -25,38 +24,50 @@ pub trait PlanGenerator {
 }
 
 #[serde_with::skip_serializing_none]
-#[derive(Debug, Serialize, Deserialize, Clone)]
+#[derive(PartialEq, Eq, Default, Debug, Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
 pub struct BuildPlan {
-    #[serde(rename = "nixpacksVersion")]
-    nixpacks_version: Option<String>,
+    pub providers: Option<Vec<String>>,
 
     #[serde(rename = "buildImage")]
-    pub build_image: String,
+    pub build_image: Option<String>,
 
     pub variables: Option<EnvironmentVariables>,
 
     #[serde(rename = "staticAssets")]
     pub static_assets: Option<StaticAssets>,
 
-    pub phases: Vec<Phase>,
+    pub phases: Option<Phases>,
 
-    #[serde(rename = "startPhase")]
+    #[serde(rename = "start")]
     pub start_phase: Option<StartPhase>,
 }
 
 impl BuildPlan {
-    pub fn new(phases: Vec<Phase>, start_phase: Option<StartPhase>) -> Self {
+    pub fn new(phases: &[Phase], start_phase: Option<StartPhase>) -> Self {
         Self {
-            nixpacks_version: Some(NIX_PACKS_VERSION.to_string()),
-            phases,
+            phases: Some(phases.iter().map(|p| (p.get_name(), p.clone())).collect()),
             start_phase,
-            build_image: DEFAULT_BASE_IMAGE.to_string(),
+            build_image: Some(DEFAULT_BASE_IMAGE.to_string()),
             ..Default::default()
         }
     }
 
+    pub fn from_toml<S: Into<String>>(toml: S) -> Result<Self> {
+        let mut plan: BuildPlan = toml::from_str(&toml.into())?;
+        plan.resolve_phase_names();
+        Ok(plan)
+    }
+
+    pub fn from_json<S: Into<String>>(json: S) -> Result<Self> {
+        let mut plan: BuildPlan = serde_json::from_str(&json.into())?;
+        plan.resolve_phase_names();
+        Ok(plan)
+    }
+
     pub fn add_phase(&mut self, phase: Phase) {
-        self.phases.push(phase);
+        let phases = self.phases.get_or_insert(BTreeMap::default());
+        phases.insert(phase.get_name(), phase);
     }
 
     pub fn set_start_phase(&mut self, start_phase: StartPhase) {
@@ -90,42 +101,52 @@ impl BuildPlan {
     }
 
     pub fn get_phase(&self, name: &str) -> Option<&Phase> {
-        self.phases.iter().find(|phase| phase.name == name)
+        match self.phases {
+            Some(ref phases) => phases.get(name),
+            None => None,
+        }
     }
 
     pub fn get_phase_mut(&mut self, name: &str) -> Option<&mut Phase> {
-        self.phases.iter_mut().find(|phase| phase.name == name)
+        self.phases.get_or_insert(BTreeMap::default()).get_mut(name)
     }
 
     pub fn remove_phase(&mut self, name: &str) -> Option<Phase> {
-        let index = self.phases.iter().position(|phase| phase.name == name);
-        if let Some(index) = index {
-            let phase = self.phases.swap_remove(index);
-            Some(phase)
-        } else {
-            None
-        }
+        self.phases.get_or_insert(BTreeMap::default()).remove(name)
     }
 
     pub fn get_sorted_phases(&self) -> Result<Vec<Phase>> {
-        topological_sort(self.phases.clone())
+        let phases_with_names = self
+            .phases
+            .clone()
+            .unwrap_or_default()
+            .iter()
+            .map(|(name, phase)| (name.clone(), phase.clone()))
+            .collect();
+
+        let res = topological_sort::<(String, Phase)>(phases_with_names)?
+            .iter()
+            .map(|(_, phase)| phase.clone())
+            .collect();
+
+        Ok(res)
     }
 
-    pub fn get_phases_with_dependencies(&self, phase_name: &str) -> Vec<Phase> {
+    pub fn get_phases_with_dependencies(&self, phase_name: &str) -> Phases {
         let p = self.get_phase(phase_name);
 
-        let mut phases = Vec::new();
+        let mut phases = Phases::new();
         let mut deps: Vec<String> = Vec::new();
 
         if let Some(p) = p {
-            phases.push(p.clone());
+            phases.insert(phase_name.to_string(), p.clone());
             deps.append(&mut p.clone().depends_on.unwrap_or_default());
 
             while !deps.is_empty() {
                 let dep = deps.pop().unwrap();
                 let p = self.get_phase(&dep);
                 if let Some(p) = p {
-                    phases.push(p.clone());
+                    phases.insert(dep, p.clone());
                     deps.append(&mut p.clone().depends_on.unwrap_or_default());
                 }
             }
@@ -141,9 +162,9 @@ impl BuildPlan {
         phase_name: &str,
     ) -> String {
         let phases = plan.get_phases_with_dependencies(phase_name);
-        for mut phase in phases {
+        for (_, mut phase) in phases {
             phase.prefix_name(prefix);
-            self.add_phase(phase.clone());
+            self.add_phase(phase);
         }
 
         format!("{}:{}", prefix, phase_name)
@@ -155,87 +176,120 @@ impl BuildPlan {
         }
     }
 
-    /// Create a new build plan by applying the given configuration
-    pub fn apply_config(plan: &BuildPlan, config: &GeneratePlanConfig) -> BuildPlan {
-        let mut new_plan = plan.clone();
-
-        // Setup config
-        let mut setup = new_plan
-            .remove_phase("setup")
-            .unwrap_or_else(|| Phase::setup(None));
-
-        // Append the packages and libraries together
-        setup.apt_pkgs = none_if_empty(
-            [
-                config.custom_apt_pkgs.clone(),
-                setup.apt_pkgs.unwrap_or_default(),
-            ]
-            .concat(),
-        );
-        setup.nix_pkgs = none_if_empty(
-            [
-                config.custom_pkgs.clone(),
-                setup.nix_pkgs.unwrap_or_default(),
-            ]
-            .concat(),
-        );
-        setup.nix_libraries = none_if_empty(
-            [
-                config.custom_libs.clone(),
-                setup.nix_libraries.unwrap_or_default(),
-            ]
-            .concat(),
-        );
-        setup.nixpacks_archive = setup.nixpacks_archive.or_else(|| {
-            if config.pin_pkgs {
-                Some(NIXPKGS_ARCHIVE.to_string())
-            } else {
-                None
-            }
-        });
-        new_plan.add_phase(setup);
-
-        // Install config
-        let mut install = new_plan
-            .remove_phase("install")
-            .unwrap_or_else(|| Phase::install(None));
-        install.cmds = config.custom_install_cmd.clone().or(install.cmds);
-        new_plan.add_phase(install);
-
-        // Build config
-        let mut build = new_plan
-            .remove_phase("build")
-            .unwrap_or_else(|| Phase::build(None));
-        build.cmds = config.custom_build_cmd.clone().or(build.cmds);
-        new_plan.add_phase(build);
-
-        // Start config
-        let mut start = new_plan.start_phase.clone().unwrap_or_default();
-        start.cmd = config.custom_start_cmd.clone().or(start.cmd);
-        new_plan.start_phase = Some(start);
-
-        new_plan
+    pub fn resolve_phase_names(&mut self) {
+        let phases = self.phases.get_or_insert(BTreeMap::default());
+        for (name, phase) in phases.iter_mut() {
+            phase.set_name(name);
+        }
     }
-}
 
-impl Default for BuildPlan {
-    fn default() -> Self {
-        Self {
-            nixpacks_version: Some(NIX_PACKS_VERSION.to_string()),
-            build_image: DEFAULT_BASE_IMAGE.to_string(),
-            phases: vec![],
-            start_phase: None,
-            variables: None,
-            static_assets: None,
+    pub fn from_environment(env: &Environment) -> Self {
+        let mut phases: Vec<Phase> = Vec::new();
+
+        // Setup
+        let mut setup = Phase::setup(None);
+        let mut uses_setup = false;
+
+        if let Some(pkg_string) = env.get_config_variable("PKGS") {
+            let mut pkgs = pkg_string
+                .split(' ')
+                .map(std::string::ToString::to_string)
+                .collect::<Vec<_>>();
+            pkgs.push("...".to_string());
+            setup.nix_pkgs = Some(pkgs);
+            uses_setup = true;
+        }
+        if let Some(apt_string) = env.get_config_variable("APT_PKGS") {
+            let mut apts = apt_string
+                .split(' ')
+                .map(std::string::ToString::to_string)
+                .collect::<Vec<_>>();
+            apts.push("...".to_string());
+            setup.apt_pkgs = Some(apts);
+            uses_setup = true;
+        }
+        if let Some(nix_lib_string) = env.get_config_variable("LIBS") {
+            let mut libs = nix_lib_string
+                .split(' ')
+                .map(std::string::ToString::to_string)
+                .collect::<Vec<_>>();
+            libs.push("...".to_string());
+            setup.nix_libs = Some(libs);
+            uses_setup = true;
+        }
+
+        if uses_setup {
+            phases.push(setup);
+        }
+
+        // Install
+        if let Some(cmd_string) = env.get_config_variable("INSTALL_CMD") {
+            let install = Phase::install(Some(cmd_string));
+            phases.push(install);
+        }
+
+        // Build
+        if let Some(cmd_string) = env.get_config_variable("BUILD_CMD") {
+            let build = Phase::build(Some(cmd_string));
+            phases.push(build);
+        }
+
+        // Start
+        let start = env.get_config_variable("START_CMD").map(StartPhase::new);
+
+        BuildPlan::new(&phases, start)
+    }
+
+    pub fn pin(&mut self) {
+        self.providers = Some(Vec::new());
+        if self.build_image.is_none() {
+            self.build_image = Some(DEBIAN_SLIM_IMAGE.to_string());
+        }
+
+        self.resolve_phase_names();
+        let phases = self.phases.get_or_insert(Phases::default());
+        for (_, phase) in phases.iter_mut() {
+            phase.pin();
+        }
+
+        if let Some(start) = &mut self.start_phase {
+            start.pin();
+        }
+    }
+
+    pub fn prefix_phases(&mut self, prefix: &str) {
+        if let Some(phases) = self.phases.clone() {
+            self.resolve_phase_names();
+            let mut new_phases = Phases::default();
+
+            for (_, phase) in phases {
+                let mut new_phase = phase.clone();
+                new_phase.prefix_name(prefix);
+                new_phase.prefix_dependencies(prefix);
+                new_phases.insert(new_phase.get_name(), new_phase);
+            }
+
+            self.phases = Some(new_phases);
         }
     }
+
+    pub fn merge_plans(plans: &[BuildPlan]) -> BuildPlan {
+        plans.iter().fold(BuildPlan::default(), |acc, plan| {
+            BuildPlan::merge(&acc, plan)
+        })
+    }
 }
 
-fn none_if_empty<T>(value: Vec<T>) -> Option<Vec<T>> {
-    if value.is_empty() {
-        None
-    } else {
-        Some(value)
+impl topological_sort::TopItem for (String, Phase) {
+    fn get_name(&self) -> String {
+        self.0.clone()
+    }
+
+    fn get_dependencies(&self) -> &[String] {
+        match &self.1.depends_on {
+            Some(depends_on) => depends_on.as_slice(),
+            None => &[],
+        }
     }
 }
 
@@ -256,15 +310,36 @@ mod test {
         let mut another = Phase::new("another");
         another.depends_on_phase("setup");
 
-        let plan = BuildPlan::new(vec![setup, install, build, another], None);
-
-        let phases = topological_sort(plan.get_phases_with_dependencies("build")).unwrap();
+        let plan = BuildPlan::new(&vec![setup, install, build, another], None);
 
-        println!("{:?}", phases);
+        // let phases = topological_sort(plan.get_phases_with_dependencies("build")).unwrap();
+        let build_phase = plan.get_phases_with_dependencies("build");
+        let phases = build_phase.values();
 
         assert_eq!(phases.len(), 3);
-        assert_eq!(phases[0].name, "setup");
-        assert_eq!(phases[1].name, "install");
-        assert_eq!(phases[2].name, "build");
+    }
+
+    #[test]
+    fn test_pin_build_plan() {
+        let mut plan = BuildPlan::from_toml(
+            r#"
+            [phases.setup]
+            nixPkgs = ["nodejs", "@auto", "yarn"]
+
+            [phases.build]
+            cmds = ["yarn run build", "...", "yarn run optimize-assets"]
+
+            [start]
+            cmd = "yarn run start"
+            "#,
+        )
+        .unwrap();
+
+        plan.pin();
+        assert_eq!(
+            plan.get_phase("setup").unwrap().nix_pkgs,
+            Some(vec!["nodejs".to_string(), "yarn".to_string()])
+        );
+        assert!(plan.get_phase("setup").unwrap().nixpkgs_archive.is_some());
     }
 }
diff --git a/src/nixpacks/plan/phase.rs b/src/nixpacks/plan/phase.rs
index d4ab31b..ebd2a4a 100644
--- a/src/nixpacks/plan/phase.rs
+++ b/src/nixpacks/plan/phase.rs
@@ -1,33 +1,36 @@
-use serde::{Deserialize, Serialize};
-
+use super::generator::NIXPKGS_ARCHIVE;
 use crate::nixpacks::{
     images::{DEBIAN_SLIM_IMAGE, DEFAULT_BASE_IMAGE},
     nix::pkg::Pkg,
 };
+use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
 
-use super::topological_sort::TopItem;
+pub type Phases = BTreeMap<String, Phase>;
 
 #[serde_with::skip_serializing_none]
-#[derive(Serialize, Deserialize, Default, Clone, Debug)]
+#[derive(PartialEq, Eq, Serialize, Deserialize, Default, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
 pub struct Phase {
-    pub name: String,
+    pub name: Option<String>,
 
     #[serde(rename = "dependsOn")]
     pub depends_on: Option<Vec<String>>,
 
-    #[serde(rename = "nixPackages")]
-    pub nix_pkgs: Option<Vec<Pkg>>,
+    #[serde(alias = "nixPackages")]
+    pub nix_pkgs: Option<Vec<String>>,
 
-    #[serde(rename = "nixLibraries")]
-    pub nix_libraries: Option<Vec<String>>,
+    #[serde(alias = "nixLibraries")]
+    pub nix_libs: Option<Vec<String>>,
 
-    #[serde(rename = "nixpacksArchive")]
-    pub nixpacks_archive: Option<String>,
+    pub nix_overlays: Option<Vec<String>>,
 
-    #[serde(rename = "aptPackages")]
+    pub nixpkgs_archive: Option<String>,
+
+    #[serde(alias = "aptPackages")]
     pub apt_pkgs: Option<Vec<String>>,
 
-    #[serde(rename = "commands")]
+    #[serde(alias = "commands")]
     pub cmds: Option<Vec<String>>,
 
     #[serde(rename = "onlyIncludeFiles")]
@@ -36,51 +39,60 @@ pub struct Phase {
     #[serde(rename = "cacheDirectories")]
     pub cache_directories: Option<Vec<String>>,
 
+    #[serde(alias = "envPaths")]
     pub paths: Option<Vec<String>>,
 }
 
 #[serde_with::skip_serializing_none]
-#[derive(Serialize, Deserialize, Default, Clone, Debug)]
+#[derive(PartialEq, Eq, Serialize, Deserialize, Default, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
 pub struct StartPhase {
     pub cmd: Option<String>,
-
-    #[serde(rename = "runImage")]
     pub run_image: Option<String>,
-
-    #[serde(rename = "onlyIncludeFiles")]
     pub only_include_files: Option<Vec<String>>,
 }
 
-impl TopItem for Phase {
-    fn get_name(&self) -> String {
-        self.name.clone()
-    }
-
-    fn get_dependencies(&self) -> &[String] {
-        match &self.depends_on {
-            Some(depends_on) => depends_on.as_slice(),
-            None => &[],
-        }
-    }
-}
-
 impl Phase {
     pub fn new<S: Into<String>>(name: S) -> Self {
         Self {
-            name: name.into(),
+            name: Some(name.into()),
             ..Default::default()
         }
     }
 
+    pub fn get_name(&self) -> String {
+        self.name.clone().unwrap_or_default()
+    }
+
     pub fn prefix_name(&mut self, prefix: &str) {
-        self.name = format!("{}:{}", prefix, self.name);
+        self.name = Some(format!("{}:{}", prefix, self.get_name()));
+    }
+
+    pub fn prefix_dependencies(&mut self, prefix: &str) {
+        if let Some(depends_on) = &self.depends_on {
+            self.depends_on = Some(
+                depends_on
+                    .clone()
+                    .iter()
+                    .map(|name| format!("{}:{}", prefix, name))
+                    .collect::<Vec<_>>(),
+            );
+        }
+    }
+
+    pub fn set_name<S: Into<String>>(&mut self, name: S) {
+        self.name = Some(name.into());
     }
 
     /// Shortcut for creating a setup phase from a list of nix packages.
     pub fn setup(pkgs: Option<Vec<Pkg>>) -> Self {
         Self {
-            nix_pkgs: pkgs,
-            name: "setup".to_string(),
+            nix_pkgs: pkgs
+                .clone()
+                .map(|pkgs| pkgs.iter().map(Pkg::to_nix_string).collect()),
+            nix_overlays: pkgs
+                .map(|pkgs| pkgs.iter().filter_map(|pkg| pkg.overlay.clone()).collect()),
+            name: Some("setup".to_string()),
             ..Default::default()
         }
     }
@@ -88,7 +100,7 @@ impl Phase {
     /// Shortcut for creating an install phase from a command
     pub fn install(cmd: Option<String>) -> Self {
         Self {
-            name: "install".to_string(),
+            name: Some("install".to_string()),
             cmds: cmd.map(|cmd| vec![cmd]),
             depends_on: Some(vec!["setup".to_string()]),
             ..Default::default()
@@ -98,7 +110,7 @@ impl Phase {
     /// Shortcut for creating a build phase from a command
     pub fn build(cmd: Option<String>) -> Self {
         Self {
-            name: "build".to_string(),
+            name: Some("build".to_string()),
             cmds: cmd.map(|cmd| vec![cmd]),
             depends_on: Some(vec!["install".to_string()]),
             ..Default::default()
@@ -107,20 +119,30 @@ impl Phase {
 
     pub fn uses_nix(&self) -> bool {
         !self.nix_pkgs.clone().unwrap_or_default().is_empty()
-            || !self.nix_libraries.clone().unwrap_or_default().is_empty()
+            || !self.nix_libs.clone().unwrap_or_default().is_empty()
     }
 
     pub fn depends_on_phase<S: Into<String>>(&mut self, name: S) {
         self.depends_on = Some(add_to_option_vec(self.depends_on.clone(), name.into()));
     }
 
-    pub fn add_nix_pkgs(&mut self, new_pkgs: Vec<Pkg>) {
-        self.nix_pkgs = Some(add_multiple_to_option_vec(self.nix_pkgs.clone(), new_pkgs));
+    pub fn add_nix_pkgs(&mut self, new_pkgs: &[Pkg]) {
+        self.nix_overlays = Some(add_multiple_to_option_vec(
+            self.nix_overlays.clone(),
+            new_pkgs
+                .iter()
+                .filter_map(|pkg| pkg.overlay.clone())
+                .collect::<Vec<_>>(),
+        ));
+        self.nix_pkgs = Some(add_multiple_to_option_vec(
+            self.nix_pkgs.clone(),
+            new_pkgs.iter().map(Pkg::to_nix_string).collect(),
+        ));
     }
 
     pub fn add_pkgs_libs(&mut self, new_libraries: Vec<String>) {
-        self.nix_libraries = Some(add_multiple_to_option_vec(
-            self.nix_libraries.clone(),
+        self.nix_libs = Some(add_multiple_to_option_vec(
+            self.nix_libs.clone(),
             new_libraries,
         ));
     }
@@ -152,7 +174,23 @@ impl Phase {
     }
 
     pub fn set_nix_archive(&mut self, archive: String) {
-        self.nixpacks_archive = Some(archive);
+        self.nixpkgs_archive = Some(archive);
+    }
+
+    pub fn pin(&mut self) {
+        if self.uses_nix() && self.nixpkgs_archive.is_none() {
+            self.nixpkgs_archive = Some(NIXPKGS_ARCHIVE.to_string());
+        }
+
+        self.cmds = pin_option_vec(&self.cmds);
+        self.depends_on = pin_option_vec(&self.depends_on);
+        self.nix_pkgs = pin_option_vec(&self.nix_pkgs);
+        self.nix_libs = pin_option_vec(&self.nix_libs);
+        self.apt_pkgs = pin_option_vec(&self.apt_pkgs);
+        self.nix_overlays = pin_option_vec(&self.nix_overlays);
+        self.only_include_files = pin_option_vec(&self.only_include_files);
+        self.cache_directories = pin_option_vec(&self.cache_directories);
+        self.paths = pin_option_vec(&self.paths);
     }
 }
 
@@ -182,6 +220,26 @@ impl StartPhase {
             file.into(),
         ));
     }
+
+    pub fn pin(&mut self) {
+        self.only_include_files = pin_option_vec(&self.only_include_files);
+    }
+}
+
+fn pin_option_vec(vec: &Option<Vec<String>>) -> Option<Vec<String>> {
+    if let Some(vec) = vec {
+        Some(remove_autos_from_vec(vec.clone()))
+    } else {
+        vec.clone()
+    }
+}
+
+/// Removes all the `"..."`'s or `"@auto"`'s from the `original`
+fn remove_autos_from_vec(original: Vec<String>) -> Vec<String> {
+    original
+        .into_iter()
+        .filter(|x| x != "@auto" && x != "...")
+        .collect::<Vec<_>>()
 }
 
 fn add_to_option_vec<T>(values: Option<Vec<T>>, v: T) -> Vec<T> {
@@ -200,3 +258,30 @@ fn add_multiple_to_option_vec<T: Clone>(values: Option<Vec<T>>, new_values: Vec<
         new_values
     }
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    fn vs(v: Vec<&str>) -> Vec<String> {
+        v.into_iter()
+            .map(std::string::ToString::to_string)
+            .collect()
+    }
+
+    #[test]
+    fn test_remove_autos_from_vec() {
+        assert_eq!(
+            vs(vec!["a", "b", "c"]),
+            remove_autos_from_vec(vs(vec!["a", "b", "c"]))
+        );
+        assert_eq!(
+            vs(vec!["a", "c"]),
+            remove_autos_from_vec(vs(vec!["a", "...", "c"]))
+        );
+        assert_eq!(
+            vs(vec!["a", "c"]),
+            remove_autos_from_vec(vs(vec!["@auto", "a", "...", "c", "@auto"]))
+        );
+    }
+}
diff --git a/src/nixpacks/plan/pretty_print.rs b/src/nixpacks/plan/pretty_print.rs
index d35249d..5b3645d 100644
--- a/src/nixpacks/plan/pretty_print.rs
+++ b/src/nixpacks/plan/pretty_print.rs
@@ -1,5 +1,5 @@
 use super::{phase::Phase, BuildPlan};
-use crate::nixpacks::{nix::pkg::Pkg, NIX_PACKS_VERSION};
+use crate::nixpacks::NIX_PACKS_VERSION;
 use anyhow::Result;
 use colored::Colorize;
 use indoc::formatdoc;
@@ -17,7 +17,7 @@ impl BuildPlan {
         let phase_contents = self
             .get_sorted_phases()?
             .iter()
-            .map(|phase| (phase.name.clone(), self.get_phase_content(phase).unwrap()))
+            .map(|phase| (phase.get_name(), self.get_phase_content(phase).unwrap()))
             .collect::<Vec<_>>();
 
         let start_contents = self
@@ -112,7 +112,7 @@ impl BuildPlan {
             .into_iter()
             .map(|(name, content)| {
                 print_row(
-                    uppercase_first_letter(name.as_str()).as_str(),
+                    name.as_str(),
                     content.as_str(),
                     edge.as_str(),
                     middle_padding.as_str(),
@@ -125,7 +125,7 @@ impl BuildPlan {
             .join(format!("\n{}\n", hor_sep).as_str());
 
         let start_row = print_row(
-            "Start",
+            "start",
             start_contents.as_str(),
             edge.as_str(),
             middle_padding.as_str(),
@@ -156,14 +156,7 @@ impl BuildPlan {
         let nix_pkgs = phase.nix_pkgs.clone().unwrap_or_default();
         let apt_pkgs = phase.apt_pkgs.clone().unwrap_or_default();
         let cmds = phase.cmds.clone().unwrap_or_default();
-        let pkgs = [
-            nix_pkgs
-                .iter()
-                .map(Pkg::to_pretty_string)
-                .collect::<Vec<_>>(),
-            apt_pkgs,
-        ]
-        .concat();
+        let pkgs = [nix_pkgs, apt_pkgs].concat();
 
         let show_label = !pkgs.is_empty() && !cmds.is_empty();
 
@@ -239,7 +232,3 @@ fn print_row(
 
     output
 }
-
-fn uppercase_first_letter(s: &str) -> String {
-    s[0..1].to_uppercase() + &s[1..]
-}
diff --git a/src/nixpacks/plan/topological_sort.rs b/src/nixpacks/plan/topological_sort.rs
index c816fee..81c3323 100644
--- a/src/nixpacks/plan/topological_sort.rs
+++ b/src/nixpacks/plan/topological_sort.rs
@@ -40,7 +40,15 @@ where
     // The number of dependencies for each item
     let mut indegree = items
         .into_iter()
-        .map(|item| (item.get_name(), item.get_dependencies().len()))
+        .map(|item| {
+            (
+                item.get_name(),
+                item.get_dependencies()
+                    .iter()
+                    .filter(|dep| lookup.contains_key(&(*dep).clone()))
+                    .count(),
+            )
+        })
         .collect::<HashMap<String, usize>>();
 
     let mut result: Vec<T> = Vec::new();
diff --git a/src/providers/clojure.rs b/src/providers/clojure.rs
index 038862a..b61ce1e 100644
--- a/src/providers/clojure.rs
+++ b/src/providers/clojure.rs
@@ -43,11 +43,12 @@ impl Provider for ClojureProvider {
 
         // based on project config, uberjar can be created under ./target/uberjar or ./target, This ensure file will be found on the same place whatevery the project config is
         let move_file_cmd = "if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi";
-        let build = Phase::build(Some(format!("{}; {}", build_cmd, move_file_cmd)));
+        let mut build = Phase::build(Some(format!("{}; {}", build_cmd, move_file_cmd)));
+        build.depends_on_phase("setup");
 
         let start = StartPhase::new("bash -c \"java $JAVA_OPTS -jar /app/target/*standalone.jar\"");
 
-        let plan = BuildPlan::new(vec![setup, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, build], Some(start));
         Ok(Some(plan))
     }
 }
diff --git a/src/providers/crystal.rs b/src/providers/crystal.rs
index c3872ad..48bff59 100644
--- a/src/providers/crystal.rs
+++ b/src/providers/crystal.rs
@@ -45,7 +45,7 @@ impl Provider for CrystalProvider {
                 .ok_or_else(|| anyhow::anyhow!("Unable to get executable name"))?
         ));
 
-        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         Ok(Some(plan))
     }
 }
diff --git a/src/providers/csharp.rs b/src/providers/csharp.rs
index 46aa29e..7437ad9 100644
--- a/src/providers/csharp.rs
+++ b/src/providers/csharp.rs
@@ -39,7 +39,7 @@ impl Provider for CSharpProvider {
             .context("Invalid project_name")?;
         let start = StartPhase::new(format!("./{}/{}", ARTIFACT_DIR, project_name));
 
-        let mut plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let mut plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         plan.add_variables(EnvironmentVariables::from([
             (
                 "ASPNETCORE_ENVIRONMENT".to_string(),
diff --git a/src/providers/dart.rs b/src/providers/dart.rs
index bca1feb..535a83c 100644
--- a/src/providers/dart.rs
+++ b/src/providers/dart.rs
@@ -42,7 +42,7 @@ impl Provider for DartProvider {
         let pubspec = DartProvider::get_pubspec(app)?;
         let start = StartPhase::new(format!("./bin/{}.exe", pubspec.name));
 
-        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         Ok(Some(plan))
     }
 }
diff --git a/src/providers/deno.rs b/src/providers/deno.rs
index 1969d27..3f397d6 100644
--- a/src/providers/deno.rs
+++ b/src/providers/deno.rs
@@ -45,17 +45,18 @@ impl Provider for DenoProvider {
     fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
         let mut plan = BuildPlan::default();
 
-        let setup_phase = Phase::setup(Some(vec![Pkg::new("deno")]));
-        plan.add_phase(setup_phase);
+        let setup = Phase::setup(Some(vec![Pkg::new("deno")]));
+        plan.add_phase(setup);
 
         if let Some(build_cmd) = DenoProvider::get_build_cmd(app)? {
-            let build_phase = Phase::build(Some(build_cmd));
-            plan.add_phase(build_phase);
+            let mut build = Phase::build(Some(build_cmd));
+            build.depends_on_phase("setup");
+            plan.add_phase(build);
         };
 
         if let Some(start_cmd) = DenoProvider::get_start_cmd(app)? {
-            let start_phase = StartPhase::new(start_cmd);
-            plan.set_start_phase(start_phase);
+            let start = StartPhase::new(start_cmd);
+            plan.set_start_phase(start);
         }
 
         Ok(Some(plan))
diff --git a/src/providers/fsharp.rs b/src/providers/fsharp.rs
index 6e084b1..891d8e6 100644
--- a/src/providers/fsharp.rs
+++ b/src/providers/fsharp.rs
@@ -39,7 +39,7 @@ impl Provider for FSharpProvider {
             .context("Invalid project_name")?;
         let start = StartPhase::new(format!("./{}/{}", ARTIFACT_DIR, project_name));
 
-        let mut plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let mut plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         plan.add_variables(EnvironmentVariables::from([
             (
                 "ASPNETCORE_ENVIRONMENT".to_string(),
diff --git a/src/providers/go.rs b/src/providers/go.rs
index 08e8760..1894e6a 100644
--- a/src/providers/go.rs
+++ b/src/providers/go.rs
@@ -42,10 +42,13 @@ impl Provider for GolangProvider {
 
         let mut build = if app.includes_file("go.mod") {
             Phase::build(Some(format!("go build -o {}", BINARY_NAME)))
-        } else {
+        } else if app.includes_file("main.go") {
             Phase::build(Some(format!("go build -o {} main.go", BINARY_NAME)))
+        } else {
+            Phase::build(None)
         };
         build.add_cache_directory(GO_BUILD_CACHE_DIR.to_string());
+        build.depends_on_phase("setup");
         plan.add_phase(build);
 
         let mut start = StartPhase::new(format!("./{}", BINARY_NAME));
diff --git a/src/providers/haskell.rs b/src/providers/haskell.rs
index ea26aac..e986692 100644
--- a/src/providers/haskell.rs
+++ b/src/providers/haskell.rs
@@ -67,7 +67,7 @@ impl Provider for HaskellStackProvider {
 
         let start = StartPhase::new(format!("/root/.local/bin/{}", name));
 
-        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
 
         Ok(Some(plan))
     }
diff --git a/src/providers/java.rs b/src/providers/java.rs
index 0690e4a..fe0f00e 100644
--- a/src/providers/java.rs
+++ b/src/providers/java.rs
@@ -32,7 +32,7 @@ impl Provider for JavaProvider {
 
     fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
         let mut setup: Phase;
-        let build = if self.is_using_gradle(app) {
+        let mut build = if self.is_using_gradle(app) {
             let pkgs = self.get_jdk_and_gradle_pkgs(app)?;
             setup = Phase::setup(Some(pkgs));
 
@@ -42,7 +42,7 @@ impl Provider for JavaProvider {
             build
         } else {
             setup = Phase::setup(Some(vec![Pkg::new("jdk")]));
-            setup.add_nix_pkgs(vec![Pkg::new("maven")]);
+            setup.add_nix_pkgs(&[Pkg::new("maven")]);
             let mvn_exe = self.get_maven_exe(app);
             let mut build = Phase::build(Some(format!("{mvn_exe} -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install", 
                 mvn_exe=mvn_exe
@@ -51,8 +51,9 @@ impl Provider for JavaProvider {
             build
         };
         let start = StartPhase::new(self.get_start_cmd(app)?);
+        build.depends_on = Some(vec!["setup".to_string()]);
 
-        let plan = BuildPlan::new(vec![setup, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, build], Some(start));
         Ok(Some(plan))
     }
 }
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index 92a11e0..450513c 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -13,6 +13,7 @@ pub mod haskell;
 pub mod java;
 pub mod node;
 pub mod php;
+pub mod procfile;
 pub mod python;
 pub mod ruby;
 pub mod rust;
@@ -22,7 +23,9 @@ pub mod zig;
 
 pub trait Provider {
     fn name(&self) -> &str;
-    fn detect(&self, app: &App, _env: &Environment) -> Result<bool>;
+    fn detect(&self, _app: &App, _env: &Environment) -> Result<bool> {
+        Ok(false)
+    }
     fn get_build_plan(&self, _app: &App, _environment: &Environment) -> Result<Option<BuildPlan>>;
     fn metadata(&self, _app: &App, _env: &Environment) -> Result<ProviderMetadata> {
         Ok(ProviderMetadata::default())
diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs
index 69ab654..08792a1 100644
--- a/src/providers/node/mod.rs
+++ b/src/providers/node/mod.rs
@@ -1,10 +1,4 @@
-use std::{
-    collections::{HashMap, HashSet},
-    path::PathBuf,
-};
-
 use self::nx::ProjectJson;
-
 use super::Provider;
 use crate::{
     nixpacks::{
@@ -23,6 +17,10 @@ use anyhow::Result;
 use path_slash::PathExt;
 use regex::Regex;
 use serde::{Deserialize, Serialize};
+use std::{
+    collections::{HashMap, HashSet},
+    path::PathBuf,
+};
 mod nx;
 
 pub const NODE_OVERLAY: &str = "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz";
@@ -51,6 +49,7 @@ pub struct PackageJson {
     pub project_type: Option<String>,
 }
 
+#[derive(Default, Debug)]
 pub struct NodeProvider {}
 
 impl Provider for NodeProvider {
@@ -124,7 +123,7 @@ impl Provider for NodeProvider {
         // Start
         let start = NodeProvider::get_start_cmd(app, env)?.map(StartPhase::new);
 
-        let mut plan = BuildPlan::new(vec![setup, install, build], start);
+        let mut plan = BuildPlan::new(&vec![setup, install, build], start);
         plan.add_variables(NodeProvider::get_node_environment_variables());
 
         Ok(Some(plan))
@@ -141,7 +140,7 @@ impl NodeProvider {
     }
 
     pub fn has_script(app: &App, script: &str) -> Result<bool> {
-        let package_json: PackageJson = app.read_json("package.json")?;
+        let package_json: PackageJson = app.read_json("package.json").unwrap_or_default();
         if let Some(scripts) = package_json.scripts {
             if scripts.get(script).is_some() {
                 return Ok(true);
@@ -185,7 +184,7 @@ impl NodeProvider {
             return Ok(Some(format!("{} run start", package_manager)));
         }
 
-        let package_json: PackageJson = app.read_json("package.json")?;
+        let package_json: PackageJson = app.read_json("package.json").unwrap_or_default();
         if let Some(main) = package_json.main {
             if app.includes_file(&main) {
                 if package_manager == "bun" {
@@ -289,7 +288,11 @@ impl NodeProvider {
 
     /// Returns the nodejs nix package and the appropriate package manager nix image.
     pub fn get_nix_packages(app: &App, env: &Environment) -> Result<Vec<Pkg>> {
-        let package_json: PackageJson = app.read_json("package.json")?;
+        let package_json: PackageJson = if app.includes_file("package.json") {
+            app.read_json("package.json")?
+        } else {
+            PackageJson::default()
+        };
         let node_pkg = NodeProvider::get_nix_node_pkg(&package_json, env)?;
 
         let pm_pkg: Pkg;
diff --git a/src/providers/php/mod.rs b/src/providers/php/mod.rs
index e9ab2c8..b890245 100644
--- a/src/providers/php/mod.rs
+++ b/src/providers/php/mod.rs
@@ -35,7 +35,7 @@ impl Provider for PhpProvider {
         let start = PhpProvider::get_start(app);
 
         let mut plan = BuildPlan::new(
-            vec![Some(setup), Some(install), build]
+            &vec![Some(setup), Some(install), build]
                 .into_iter()
                 .flatten()
                 .collect::<Vec<_>>(),
diff --git a/src/providers/procfile.rs b/src/providers/procfile.rs
new file mode 100644
index 0000000..8664a1b
--- /dev/null
+++ b/src/providers/procfile.rs
@@ -0,0 +1,74 @@
+use std::collections::HashMap;
+
+use super::Provider;
+use crate::nixpacks::{
+    app::App,
+    environment::Environment,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
+    },
+};
+use anyhow::{Context, Ok, Result};
+
+pub struct ProcfileProvider {}
+
+impl Provider for ProcfileProvider {
+    fn name(&self) -> &str {
+        "deno"
+    }
+
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let mut plan = BuildPlan::default();
+
+        if let Some(release_cmd) = ProcfileProvider::get_release_cmd(app)? {
+            let mut release = Phase::new("release");
+            release.depends_on = Some(vec![
+                "setup".to_owned(),
+                "install".to_owned(),
+                "build".to_owned(),
+            ]);
+            release.cmds = Some(vec!["...".to_string(), release_cmd]);
+            plan.add_phase(release);
+        };
+
+        if let Some(start_cmd) = ProcfileProvider::get_start_cmd(app)? {
+            let start_phase = StartPhase::new(start_cmd);
+            plan.set_start_phase(start_phase);
+        }
+
+        Ok(Some(plan))
+    }
+}
+
+impl ProcfileProvider {
+    fn get_start_cmd(app: &App) -> Result<Option<String>> {
+        if app.includes_file("Procfile") {
+            let mut procfile: HashMap<String, String> =
+                app.read_yaml("Procfile").context("Reading Procfile")?;
+            procfile.remove("release");
+            if procfile.is_empty() {
+                Ok(None)
+            } else {
+                let process = procfile.values().collect::<Vec<_>>()[0].to_string();
+                Ok(Some(process))
+            }
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn get_release_cmd(app: &App) -> Result<Option<String>> {
+        if app.includes_file("Procfile") {
+            let procfile: HashMap<String, String> =
+                app.read_yaml("Procfile").context("Reading Procfile")?;
+            if let Some(release) = procfile.get("release") {
+                Ok(Some(release.to_string()))
+            } else {
+                Ok(None)
+            }
+        } else {
+            Ok(None)
+        }
+    }
+}
diff --git a/src/providers/python.rs b/src/providers/python.rs
index 12ae528..e2a7c6f 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -51,12 +51,12 @@ impl Provider for PythonProvider {
     fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
         let mut plan = BuildPlan::default();
 
-        if let Some(setup) = self.setup(app, env)? {
-            plan.add_phase(setup);
-        }
-        if let Some(install) = self.install(app, env)? {
-            plan.add_phase(install);
-        }
+        let setup = self.setup(app, env)?.unwrap_or_default();
+        plan.add_phase(setup);
+
+        let install = self.install(app, env)?.unwrap_or_default();
+        plan.add_phase(install);
+
         if let Some(start) = self.start(app, env)? {
             plan.set_start_phase(start);
         }
@@ -122,14 +122,14 @@ impl PythonProvider {
             pkgs.append(&mut vec![Pkg::new("libmysqlclient.dev")]);
         }
 
-        let mut setup_phase = Phase::setup(Some(pkgs));
+        let mut setup = Phase::setup(Some(pkgs));
 
         // Many Python packages need some C headers to be available
         // stdenv.cc.cc.lib -> https://discourse.nixos.org/t/nixos-with-poetry-installed-pandas-libstdc-so-6-cannot-open-shared-object-file/8442/3
-        setup_phase.add_pkgs_libs(vec!["zlib".to_string(), "stdenv.cc.cc.lib".to_string()]);
-        setup_phase.add_nix_pkgs(vec![Pkg::new("gcc")]);
+        setup.add_pkgs_libs(vec!["zlib".to_string(), "stdenv.cc.cc.lib".to_string()]);
+        setup.add_nix_pkgs(&[Pkg::new("gcc")]);
 
-        Ok(Some(setup_phase))
+        Ok(Some(setup))
     }
 
     fn install(&self, app: &App, _env: &Environment) -> Result<Option<Phase>> {
@@ -174,7 +174,7 @@ impl PythonProvider {
             return Ok(Some(install_phase));
         }
 
-        Ok(None)
+        Ok(Some(Phase::install(None)))
     }
 
     fn start(&self, app: &App, env: &Environment) -> Result<Option<StartPhase>> {
diff --git a/src/providers/ruby.rs b/src/providers/ruby.rs
index c06ef5a..42b2433 100644
--- a/src/providers/ruby.rs
+++ b/src/providers/ruby.rs
@@ -30,21 +30,21 @@ impl Provider for RubyProvider {
         let start = self.get_start(app)?;
 
         let mut plan = BuildPlan::new(
-            vec![setup, install, build]
+            &vec![setup, install, build]
                 .into_iter()
                 .flatten()
                 .collect::<Vec<_>>(),
             start,
         );
 
-        let node = NodeProvider {};
+        let node = NodeProvider::default();
         if node.detect(app, env)? {
             let node_build_plan = node.get_build_plan(app, env)?;
             if let Some(node_build_plan) = node_build_plan {
                 // Include the install phase from the node provider
-                let phase_name =
+                let root_phase_name =
                     plan.add_phases_from_another_plan(&node_build_plan, node.name(), "install");
-                plan.add_dependency_between_phases("build", phase_name.as_str());
+                plan.add_dependency_between_phases("build", root_phase_name.as_str());
             }
         }
 
diff --git a/src/providers/rust.rs b/src/providers/rust.rs
index e9004b0..4b9719c 100644
--- a/src/providers/rust.rs
+++ b/src/providers/rust.rs
@@ -37,7 +37,7 @@ impl Provider for RustProvider {
         let build = RustProvider::get_build(app, env)?;
         let start = RustProvider::get_start(app, env)?;
 
-        let mut plan = BuildPlan::new(vec![setup, build], start);
+        let mut plan = BuildPlan::new(&vec![setup, build], start);
         plan.add_variables(EnvironmentVariables::from([(
             "ROCKET_ADDRESS".to_string(),
             "0.0.0.0".to_string(),
@@ -83,6 +83,7 @@ impl RustProvider {
         let mut build = Phase::build(None);
 
         build.add_cmd("mkdir -p bin");
+        build.depends_on = Some(vec!["setup".to_string()]);
 
         let mut build_cmd = "cargo build --release".to_string();
 
diff --git a/src/providers/staticfile.rs b/src/providers/staticfile.rs
index ee5c682..9bdf365 100644
--- a/src/providers/staticfile.rs
+++ b/src/providers/staticfile.rs
@@ -53,7 +53,7 @@ impl Provider for StaticfileProvider {
 
         let static_assets = StaticfileProvider::get_static_assets(app, env)?;
 
-        let mut plan = BuildPlan::new(vec![setup, build], Some(start));
+        let mut plan = BuildPlan::new(&vec![setup, build], Some(start));
         plan.add_static_assets(static_assets);
 
         Ok(Some(plan))
diff --git a/src/providers/swift.rs b/src/providers/swift.rs
index f0e8fcb..d427a70 100644
--- a/src/providers/swift.rs
+++ b/src/providers/swift.rs
@@ -86,7 +86,7 @@ impl Provider for SwiftProvider {
         let name = SwiftProvider::get_executable_name(app)?;
         let start = StartPhase::new(format!("./{}", name));
 
-        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
 
         Ok(Some(plan))
     }
diff --git a/src/providers/zig.rs b/src/providers/zig.rs
index 8be7cd2..717f6a0 100644
--- a/src/providers/zig.rs
+++ b/src/providers/zig.rs
@@ -29,7 +29,7 @@ impl Provider for ZigProvider {
         let mut setup = Phase::setup(Some(vec![Pkg::new("zig")]));
 
         if app.includes_file("gyro.zzz") {
-            setup.add_nix_pkgs(vec![Pkg::new("wget")]);
+            setup.add_nix_pkgs(&[Pkg::new("wget")]);
         }
 
         let mut install = Phase::install(None);
@@ -56,7 +56,7 @@ impl Provider for ZigProvider {
                 .map_or("*", Option::unwrap)
         ));
 
-        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         Ok(Some(plan))
     }
 }


Test patch:
diff --git a/test-helper/src/lib.rs b/test-helper/src/lib.rs
index 5e69234..de7e5eb 100644
--- a/test-helper/src/lib.rs
+++ b/test-helper/src/lib.rs
@@ -3,6 +3,7 @@ use quote::{format_ident, quote};
 use walkdir::{DirEntry, WalkDir};
 
 const IGNORE: &[&str] = &[
+    "custom-plan-path",
     "rust-custom-version",
     "rust-rocket",
     "haskell-stack",
@@ -45,9 +46,8 @@ pub fn generate_plan_tests(_tokens: TokenStream) -> TokenStream {
         macro_rules! assert_plan_snapshot {
             ($plan:expr) => {
                 ::insta::assert_json_snapshot!($plan, {
-                    ".nixpacksVersion" => "[version]",
                     ".buildImage" => "[build_image]",
-                    ".phases.*.nixpacksArchive" => "[archive]",
+                    ".phases.*.nixpkgsArchive" => "[archive]",
                 });
             }
         }
@@ -55,29 +55,45 @@ pub fn generate_plan_tests(_tokens: TokenStream) -> TokenStream {
         fn simple_gen_plan(path: &str) -> ::nixpacks::nixpacks::plan::BuildPlan {
             if let Ok(raw_env) = ::std::fs::read_to_string(format!("{}/test.env", path)) {
                 let env = ::dotenv_parser::parse_dotenv(&raw_env).unwrap();
-                let opts = ::nixpacks::nixpacks::plan::config::GeneratePlanConfig {
-                    pin_pkgs: env.get("PIN_PKGS").is_some(),
-                    custom_start_cmd: env.get("CUSTOM_START_CMD").map(|cmd| cmd.to_string()),
-                    custom_pkgs: env
-                        .get("CUSTOM_PKGS")
-                        .map(|pkgs| pkgs.split(',')
-                        .map(|pkg| ::nixpacks::nixpacks::nix::pkg::Pkg::new(pkg)).collect())
-                        .unwrap_or_default(),
-                    ..::nixpacks::nixpacks::plan::config::GeneratePlanConfig::default()
+                let plan = ::nixpacks::nixpacks::plan::BuildPlan {
+                    phases: Some(::std::collections::BTreeMap::from([(
+                        "setup".to_string(),
+                        ::nixpacks::nixpacks::plan::phase::Phase {
+                            nix_pkgs: env.get("CUSTOM_PKGS").map(|pkgs| {
+                                pkgs.split(',')
+                                    .map(|pkg| pkg.to_string())
+                                    .collect::<Vec<_>>()
+                            }),
+                            ..Default::default()
+                        },
+                    )])),
+                    start_phase: Some(::nixpacks::nixpacks::plan::phase::StartPhase {
+                        cmd: env.get("CUSTOM_START_CMD").map(|cmd| cmd.to_string()),
+                        ..Default::default()
+                    }),
+                    ..::nixpacks::nixpacks::plan::BuildPlan::default()
+                };
+                let opts = ::nixpacks::nixpacks::plan::generator::GeneratePlanOptions {
+                    plan: Some(plan),
+                    ..Default::default()
                 };
 
                 return ::nixpacks::generate_build_plan(
                     path,
-                    env.get("ENVS").map(|envs| envs.split(", ").collect()).unwrap_or_default(),
-                    &opts
-                ).unwrap();
+                    env.get("ENVS")
+                        .map(|envs| envs.split(", ").collect())
+                        .unwrap_or_default(),
+                    &opts,
+                )
+                .unwrap();
             }
 
             ::nixpacks::generate_build_plan(
                 path,
                 ::std::vec::Vec::new(),
-                &::nixpacks::nixpacks::plan::config::GeneratePlanConfig::default()
-            ).unwrap()
+                &::nixpacks::nixpacks::plan::generator::GeneratePlanOptions::default(),
+            )
+            .unwrap()
         }
     });
 
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 987b23c..c975cc8 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -2,8 +2,8 @@ use anyhow::Context;
 use nixpacks::{
     create_docker_image,
     nixpacks::{
-        builder::docker::DockerBuilderOptions, environment::EnvironmentVariables, nix::pkg::Pkg,
-        plan::config::GeneratePlanConfig,
+        builder::docker::DockerBuilderOptions, environment::EnvironmentVariables,
+        plan::generator::GeneratePlanOptions,
     },
 };
 use std::io::{BufRead, BufReader};
@@ -111,10 +111,7 @@ fn simple_build(path: &str) -> String {
     create_docker_image(
         path,
         Vec::new(),
-        &GeneratePlanConfig {
-            pin_pkgs: true,
-            ..Default::default()
-        },
+        &GeneratePlanOptions::default(),
         &DockerBuilderOptions {
             name: Some(name.clone()),
             quiet: true,
@@ -131,10 +128,7 @@ fn build_with_build_time_env_vars(path: &str, env_vars: Vec<&str>) -> String {
     create_docker_image(
         path,
         env_vars,
-        &GeneratePlanConfig {
-            pin_pkgs: true,
-            ..Default::default()
-        },
+        &GeneratePlanOptions::default(),
         &DockerBuilderOptions {
             name: Some(name.clone()),
             quiet: true,
@@ -586,10 +580,7 @@ fn test_rust_custom_version() {
     create_docker_image(
         "./examples/rust-custom-version",
         vec!["NIXPACKS_NO_MUSL=1"],
-        &GeneratePlanConfig {
-            pin_pkgs: true,
-            ..Default::default()
-        },
+        &GeneratePlanOptions::default(),
         &DockerBuilderOptions {
             name: Some(name.clone()),
             quiet: true,
@@ -664,12 +655,7 @@ fn test_cowsay() {
     create_docker_image(
         "./examples/shell-hello",
         Vec::new(),
-        &GeneratePlanConfig {
-            pin_pkgs: true,
-            custom_start_cmd: Some("./start.sh".to_string()),
-            custom_pkgs: vec![Pkg::new("cowsay")],
-            ..Default::default()
-        },
+        &GeneratePlanOptions::default(),
         &DockerBuilderOptions {
             name: Some(name.clone()),
             quiet: true,
@@ -695,10 +681,7 @@ fn test_swift() {
     create_docker_image(
         "./examples/swift",
         Vec::new(),
-        &GeneratePlanConfig {
-            pin_pkgs: false,
-            ..Default::default()
-        },
+        &GeneratePlanOptions::default(),
         &DockerBuilderOptions {
             name: Some(name.clone()),
             quiet: true,
diff --git a/tests/generate_plan_tests.rs b/tests/generate_plan_tests.rs
index 905b306..5466f5d 100644
--- a/tests/generate_plan_tests.rs
+++ b/tests/generate_plan_tests.rs
@@ -1,8 +1,23 @@
-use nixpacks::{generate_build_plan, nixpacks::plan::config::GeneratePlanConfig};
+use nixpacks::{generate_build_plan, nixpacks::plan::generator::GeneratePlanOptions};
 use std::env::consts::ARCH;
 
 test_helper::generate_plan_tests!();
 
+#[test]
+fn test_custom_plan_path() {
+    let plan = generate_build_plan(
+        "./examples/custom-plan-path",
+        Vec::new(),
+        &GeneratePlanOptions {
+            config_file: Some("custom-nixpacks.toml".to_string()),
+            ..Default::default()
+        },
+    )
+    .unwrap();
+
+    assert_plan_snapshot!(plan);
+}
+
 #[test]
 fn test_custom_rust_version() {
     let plan = simple_gen_plan("./examples/rust-custom-version");
@@ -25,7 +40,7 @@ fn test_custom_rust_version() {
             .nix_pkgs
             .unwrap()
             .iter()
-            .filter(|p| p.name.contains("1.56.0"))
+            .filter(|p| p.contains("1.56.0"))
             .count(),
         1
     );
@@ -55,7 +70,7 @@ fn test_rust_rocket_no_musl() {
     let plan = generate_build_plan(
         "./examples/rust-rocket",
         vec!["NIXPACKS_NO_MUSL=1"],
-        &GeneratePlanConfig::default(),
+        &GeneratePlanOptions::default(),
     )
     .unwrap();
     assert_plan_snapshot!(plan);
diff --git a/tests/snapshots/generate_plan_tests__clojure.snap b/tests/snapshots/generate_plan_tests__clojure.snap
index cf2a0e4..8ebe036 100644
--- a/tests/snapshots/generate_plan_tests__clojure.snap
+++ b/tests/snapshots/generate_plan_tests__clojure.snap
@@ -3,40 +3,33 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "clojure"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
+      ],
+      "cmds": [
+        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "leiningen"
-        },
-        {
-          "name": "jdk8"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "leiningen",
+        "jdk8"
       ],
-      "commands": [
-        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bash -c \"java $JAVA_OPTS -jar /app/target/*standalone.jar\""
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__clojure_jdk11.snap b/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
index f0f48da..2860fe4 100644
--- a/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
+++ b/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
@@ -3,40 +3,33 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "clojure"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
+      ],
+      "cmds": [
+        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "leiningen"
-        },
-        {
-          "name": "jdk11"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "leiningen",
+        "jdk11"
       ],
-      "commands": [
-        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bash -c \"java $JAVA_OPTS -jar /app/target/*standalone.jar\""
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap b/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
index f907339..a9f7c4e 100644
--- a/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
+++ b/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
@@ -3,40 +3,33 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "clojure"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
+      ],
+      "cmds": [
+        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "leiningen"
-        },
-        {
-          "name": "jdk"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "leiningen",
+        "jdk"
       ],
-      "commands": [
-        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bash -c \"java $JAVA_OPTS -jar /app/target/*standalone.jar\""
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__clojure_ring_app.snap b/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
index 6ef6441..ee64ca8 100644
--- a/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
+++ b/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
@@ -3,40 +3,33 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "clojure"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
+      ],
+      "cmds": [
+        "lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "leiningen"
-        },
-        {
-          "name": "jdk8"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "leiningen",
+        "jdk8"
       ],
-      "commands": [
-        "lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bash -c \"java $JAVA_OPTS -jar /app/target/*standalone.jar\""
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
index e305bc1..1c7e4b0 100644
--- a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
+++ b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_BUILD_CACHE_DIRS": "/build,barbaz",
@@ -14,38 +14,35 @@ expression: plan
     "NIXPACKS_RUN_IMAGE": "alpine",
     "NIXPACKS_START_CMD": "start"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "cowsay"
-        },
-        {
-          "name": "ripgrep"
-        }
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "build"
       ]
     },
-    {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "install"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "cowsay",
+        "ripgrep"
       ],
-      "commands": [
-        "build"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__config_json_file.snap b/tests/snapshots/generate_plan_tests__config_json_file.snap
new file mode 100644
index 0000000..1e2bbe1
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__config_json_file.snap
@@ -0,0 +1,20 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "phases": {
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "cowsay"
+      ],
+      "nixpkgsArchive": "[archive]"
+    }
+  },
+  "start": {
+    "cmd": "not the start command"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__crystal.snap b/tests/snapshots/generate_plan_tests__crystal.snap
index 515abda..95f426a 100644
--- a/tests/snapshots/generate_plan_tests__crystal.snap
+++ b/tests/snapshots/generate_plan_tests__crystal.snap
@@ -3,43 +3,41 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "crystal"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "shards build"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "shards install"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "crystal"
-        },
-        {
-          "name": "shards"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "crystal",
+        "shards"
       ],
-      "commands": [
-        "shards build"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./bin/crystal"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__csharp_api.snap b/tests/snapshots/generate_plan_tests__csharp_api.snap
index 9343e73..b34c482 100644
--- a/tests/snapshots/generate_plan_tests__csharp_api.snap
+++ b/tests/snapshots/generate_plan_tests__csharp_api.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "ASPNETCORE_ENVIRONMENT": "Production",
@@ -11,35 +11,35 @@ expression: plan
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/",
     "NIXPACKS_METADATA": "csharp"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "dotnet publish --no-restore -c Release -o out"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "dotnet restore"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "dotnet-sdk"
       ],
-      "commands": [
-        "dotnet publish --no-restore -c Release -o out"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out/csharp-api"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__csharp_cli.snap b/tests/snapshots/generate_plan_tests__csharp_cli.snap
index ac8d7bd..f845360 100644
--- a/tests/snapshots/generate_plan_tests__csharp_cli.snap
+++ b/tests/snapshots/generate_plan_tests__csharp_cli.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "ASPNETCORE_ENVIRONMENT": "Production",
@@ -11,35 +11,35 @@ expression: plan
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/",
     "NIXPACKS_METADATA": "csharp"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "dotnet publish --no-restore -c Release -o out"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "dotnet restore"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "dotnet-sdk"
       ],
-      "commands": [
-        "dotnet publish --no-restore -c Release -o out"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out/csharp-cli"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__custom_pkgs.snap b/tests/snapshots/generate_plan_tests__custom_pkgs.snap
index b162487..0ab45a8 100644
--- a/tests/snapshots/generate_plan_tests__custom_pkgs.snap
+++ b/tests/snapshots/generate_plan_tests__custom_pkgs.snap
@@ -3,31 +3,18 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
-  "phases": [
-    {
+  "phases": {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "cowsay"
-        }
-      ]
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+      "nixPkgs": [
+        "cowsay"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./start.sh"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__custom_plan_path.snap b/tests/snapshots/generate_plan_tests__custom_plan_path.snap
new file mode 100644
index 0000000..e8562b7
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__custom_plan_path.snap
@@ -0,0 +1,12 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "phases": {},
+  "start": {
+    "cmd": "correct start command"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__dart.snap b/tests/snapshots/generate_plan_tests__dart.snap
index 6bd6020..c6cfcaf 100644
--- a/tests/snapshots/generate_plan_tests__dart.snap
+++ b/tests/snapshots/generate_plan_tests__dart.snap
@@ -3,43 +3,43 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "dart"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "dart compile exe bin/console_simple.dart"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "dart pub get"
       ],
       "onlyIncludeFiles": [
         "pubspec.yaml"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dart"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "dart"
       ],
-      "commands": [
-        "dart compile exe bin/console_simple.dart"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./bin/console_simple.exe"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__deno.snap b/tests/snapshots/generate_plan_tests__deno.snap
index 88499fb..7932654 100644
--- a/tests/snapshots/generate_plan_tests__deno.snap
+++ b/tests/snapshots/generate_plan_tests__deno.snap
@@ -3,37 +3,32 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "deno"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
+      ],
+      "cmds": [
+        "deno cache src/index.ts"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "deno"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "deno"
       ],
-      "commands": [
-        "deno cache src/index.ts"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "deno run --allow-all src/index.ts"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__deno_fresh.snap b/tests/snapshots/generate_plan_tests__deno_fresh.snap
index fe07c43..a4860d6 100644
--- a/tests/snapshots/generate_plan_tests__deno_fresh.snap
+++ b/tests/snapshots/generate_plan_tests__deno_fresh.snap
@@ -3,34 +3,22 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "deno"
   },
-  "phases": [
-    {
+  "phases": {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "deno"
-        }
-      ]
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+      "nixPkgs": [
+        "deno"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "deno run -A dev.ts"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__elixir_ecto.snap b/tests/snapshots/generate_plan_tests__elixir_ecto.snap
index 1168e16..f7ddb0e 100644
--- a/tests/snapshots/generate_plan_tests__elixir_ecto.snap
+++ b/tests/snapshots/generate_plan_tests__elixir_ecto.snap
@@ -3,45 +3,45 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "MIX_ENV": "prod",
     "NIXPACKS_METADATA": "elixir"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "mix compile",
+        "mix ecto.migrate",
+        "mix run priv/repo/seeds.exs"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "mix local.hex --force",
         "mix local.rebar --force",
         "mix deps.get --only prod"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "elixir"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "elixir"
       ],
-      "commands": [
-        "mix compile",
-        "mix ecto.migrate",
-        "mix run priv/repo/seeds.exs"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "mix phx.server"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap b/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
index 96b4c24..a2c0429 100644
--- a/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
+++ b/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
@@ -3,44 +3,44 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "MIX_ENV": "prod",
     "NIXPACKS_METADATA": "elixir"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "mix compile",
+        "mix assets.deploy"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "mix local.hex --force",
         "mix local.rebar --force",
         "mix deps.get --only prod"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "elixir_1_12"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "elixir_1_12"
       ],
-      "commands": [
-        "mix compile",
-        "mix assets.deploy"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "mix phx.server"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__fsharp_api.snap b/tests/snapshots/generate_plan_tests__fsharp_api.snap
index 1499330..e318e0b 100644
--- a/tests/snapshots/generate_plan_tests__fsharp_api.snap
+++ b/tests/snapshots/generate_plan_tests__fsharp_api.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "ASPNETCORE_ENVIRONMENT": "Production",
@@ -11,35 +11,35 @@ expression: plan
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/",
     "NIXPACKS_METADATA": "fsharp"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "dotnet publish --no-restore -c Release -o out"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "dotnet restore"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "dotnet-sdk"
       ],
-      "commands": [
-        "dotnet publish --no-restore -c Release -o out"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out/fsharp-api"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__fsharp_cli.snap b/tests/snapshots/generate_plan_tests__fsharp_cli.snap
index bdc908e..4235e46 100644
--- a/tests/snapshots/generate_plan_tests__fsharp_cli.snap
+++ b/tests/snapshots/generate_plan_tests__fsharp_cli.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "ASPNETCORE_ENVIRONMENT": "Production",
@@ -11,35 +11,35 @@ expression: plan
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/",
     "NIXPACKS_METADATA": "fsharp"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "dotnet publish --no-restore -c Release -o out"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "dotnet restore"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "dotnet-sdk"
       ],
-      "commands": [
-        "dotnet publish --no-restore -c Release -o out"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out/fsharp-cli"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__go.snap b/tests/snapshots/generate_plan_tests__go.snap
index a554a2d..0dbbd99 100644
--- a/tests/snapshots/generate_plan_tests__go.snap
+++ b/tests/snapshots/generate_plan_tests__go.snap
@@ -3,41 +3,36 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CGO_ENABLED": "0",
     "NIXPACKS_METADATA": "golang"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "go"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "install",
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "go build -o out main.go"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "go"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out",
     "runImage": "debian:bullseye-slim"
   }
diff --git a/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap b/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap
index 3e9a1fe..85db751 100644
--- a/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap
+++ b/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap
@@ -3,41 +3,36 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CGO_ENABLED": "1",
     "NIXPACKS_METADATA": "golang"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "go"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "install",
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "go build -o out main.go"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "go"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__go_custom_version.snap b/tests/snapshots/generate_plan_tests__go_custom_version.snap
index 74d2afb..33c1738 100644
--- a/tests/snapshots/generate_plan_tests__go_custom_version.snap
+++ b/tests/snapshots/generate_plan_tests__go_custom_version.snap
@@ -3,47 +3,48 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CGO_ENABLED": "0",
     "NIXPACKS_METADATA": "golang"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
-      "commands": [
-        "go mod download"
+      "cmds": [
+        "go build -o out"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
     },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "go_1_18"
-        }
-      ]
-    },
-    {
-      "name": "build",
+    "install": {
+      "name": "install",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
-        "go build -o out"
+      "cmds": [
+        "go mod download"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "go_1_18"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out",
     "runImage": "debian:bullseye-slim"
   }
diff --git a/tests/snapshots/generate_plan_tests__go_gin.snap b/tests/snapshots/generate_plan_tests__go_gin.snap
index 74d2afb..33c1738 100644
--- a/tests/snapshots/generate_plan_tests__go_gin.snap
+++ b/tests/snapshots/generate_plan_tests__go_gin.snap
@@ -3,47 +3,48 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CGO_ENABLED": "0",
     "NIXPACKS_METADATA": "golang"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
-      "commands": [
-        "go mod download"
+      "cmds": [
+        "go build -o out"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
     },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "go_1_18"
-        }
-      ]
-    },
-    {
-      "name": "build",
+    "install": {
+      "name": "install",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
-        "go build -o out"
+      "cmds": [
+        "go mod download"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "go_1_18"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out",
     "runImage": "debian:bullseye-slim"
   }
diff --git a/tests/snapshots/generate_plan_tests__go_mod.snap b/tests/snapshots/generate_plan_tests__go_mod.snap
index e556d38..0d62cf5 100644
--- a/tests/snapshots/generate_plan_tests__go_mod.snap
+++ b/tests/snapshots/generate_plan_tests__go_mod.snap
@@ -3,47 +3,48 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CGO_ENABLED": "0",
     "NIXPACKS_METADATA": "golang"
   },
-  "phases": [
-    {
-      "name": "install",
+  "phases": {
+    "build": {
+      "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
-      "commands": [
-        "go mod download"
+      "cmds": [
+        "go build -o out"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
     },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "go"
-        }
-      ]
-    },
-    {
-      "name": "build",
+    "install": {
+      "name": "install",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
-        "go build -o out"
+      "cmds": [
+        "go mod download"
       ],
       "cacheDirectories": [
         "/root/.cache/go-build"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "go"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./out",
     "runImage": "debian:bullseye-slim"
   }
diff --git a/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap b/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
index f143e12..4a95819 100644
--- a/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
+++ b/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
@@ -3,43 +3,35 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "Java"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "jdk"
-        },
-        {
-          "name": "gradle"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "./gradlew build"
       ],
       "cacheDirectories": [
         ".gradle"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "jdk",
+        "gradle"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "java $JAVA_OPTS -jar  build/libs/*.jar"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__java_maven.snap b/tests/snapshots/generate_plan_tests__java_maven.snap
index a0442f3..c2884d1 100644
--- a/tests/snapshots/generate_plan_tests__java_maven.snap
+++ b/tests/snapshots/generate_plan_tests__java_maven.snap
@@ -3,43 +3,35 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "Java"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "jdk"
-        },
-        {
-          "name": "maven"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "mvn -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install"
       ],
       "cacheDirectories": [
         ".m2/repository"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "jdk",
+        "maven"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
index 2190093..99cc5ea 100644
--- a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
+++ b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
@@ -3,43 +3,35 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "Java"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "jdk"
-        },
-        {
-          "name": "maven"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "./mvnw -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install"
       ],
       "cacheDirectories": [
         ".m2/repository"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "jdk",
+        "maven"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__java_spring_boot.snap b/tests/snapshots/generate_plan_tests__java_spring_boot.snap
index 9dd1063..e9deb7b 100644
--- a/tests/snapshots/generate_plan_tests__java_spring_boot.snap
+++ b/tests/snapshots/generate_plan_tests__java_spring_boot.snap
@@ -3,43 +3,35 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "Java"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "jdk11"
-        },
-        {
-          "name": "gradle_6"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "./gradlew build"
       ],
       "cacheDirectories": [
         ".gradle"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "jdk11",
+        "gradle_6"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "java $JAVA_OPTS -jar -Dserver.port=$PORT build/libs/*.jar"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node.snap b/tests/snapshots/generate_plan_tests__node.snap
index cf8b9e9..4f38534 100644
--- a/tests/snapshots/generate_plan_tests__node.snap
+++ b/tests/snapshots/generate_plan_tests__node.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "npm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_bun.snap b/tests/snapshots/generate_plan_tests__node_bun.snap
index 5304636..3ae073e 100644
--- a/tests/snapshots/generate_plan_tests__node_bun.snap
+++ b/tests/snapshots/generate_plan_tests__node_bun.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bun i --no-save"
       ],
       "cacheDirectories": [
@@ -27,26 +36,18 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "bun",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "bun"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bun run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_bun_no_start.snap b/tests/snapshots/generate_plan_tests__node_bun_no_start.snap
index b86d122..a2d1d0f 100644
--- a/tests/snapshots/generate_plan_tests__node_bun_no_start.snap
+++ b/tests/snapshots/generate_plan_tests__node_bun_no_start.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bun i --no-save"
       ],
       "cacheDirectories": [
@@ -27,26 +36,18 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "bun",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "bun"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bun index.ts"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_bun_web_server.snap b/tests/snapshots/generate_plan_tests__node_bun_web_server.snap
index b86d122..a2d1d0f 100644
--- a/tests/snapshots/generate_plan_tests__node_bun_web_server.snap
+++ b/tests/snapshots/generate_plan_tests__node_bun_web_server.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bun i --no-save"
       ],
       "cacheDirectories": [
@@ -27,26 +36,18 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "bun",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "bun"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bun index.ts"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_canvas.snap b/tests/snapshots/generate_plan_tests__node_canvas.snap
index e8c1124..ca61444 100644
--- a/tests/snapshots/generate_plan_tests__node_canvas.snap
+++ b/tests/snapshots/generate_plan_tests__node_canvas.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -27,33 +36,23 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "nixLibraries": [
+      "nixLibs": [
         "libuuid",
         "libGL"
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "npm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_custom_version.snap b/tests/snapshots/generate_plan_tests__node_custom_version.snap
index 5bdafcc..2e43a8b 100644
--- a/tests/snapshots/generate_plan_tests__node_custom_version.snap
+++ b/tests/snapshots/generate_plan_tests__node_custom_version.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-18_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-18_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "npm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_main_file.snap b/tests/snapshots/generate_plan_tests__node_main_file.snap
index 253939c..bfbecbf 100644
--- a/tests/snapshots/generate_plan_tests__node_main_file.snap
+++ b/tests/snapshots/generate_plan_tests__node_main_file.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "node src/index.js"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap b/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap
index 90c719e..d8ba8ed 100644
--- a/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap
+++ b/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "node index.js"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_monorepo.snap b/tests/snapshots/generate_plan_tests__node_monorepo.snap
index 4259ebf..11592d0 100644
--- a/tests/snapshots/generate_plan_tests__node_monorepo.snap
+++ b/tests/snapshots/generate_plan_tests__node_monorepo.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -12,13 +12,23 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "packages/client/.next/cache",
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "yarn install --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -28,30 +38,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "yarn-1_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "yarn-1_x"
       ],
-      "cacheDirectories": [
-        "packages/client/.next/cache",
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "echo hello world"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_no_scripts.snap b/tests/snapshots/generate_plan_tests__node_no_scripts.snap
index 90c719e..d8ba8ed 100644
--- a/tests/snapshots/generate_plan_tests__node_no_scripts.snap
+++ b/tests/snapshots/generate_plan_tests__node_no_scripts.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "node index.js"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_npm.snap b/tests/snapshots/generate_plan_tests__node_npm.snap
index 14caac3..1838e60 100644
--- a/tests/snapshots/generate_plan_tests__node_npm.snap
+++ b/tests/snapshots/generate_plan_tests__node_npm.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "npm run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "commands": [
-        "npm run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "npm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap b/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap
index 01b056b..5cbb90f 100644
--- a/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap
+++ b/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "npm run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-6_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-6_x"
       ],
-      "commands": [
-        "npm run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "npm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_nx.snap b/tests/snapshots/generate_plan_tests__node_nx.snap
index f45c095..48768ed 100644
--- a/tests/snapshots/generate_plan_tests__node_nx.snap
+++ b/tests/snapshots/generate_plan_tests__node_nx.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,26 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "npx nx run express-app:build:production"
+      ],
+      "cacheDirectories": [
+        ".next/cache",
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -28,37 +41,23 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "nixLibraries": [
+      "nixLibs": [
         "libuuid",
         "libGL"
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
       ],
-      "commands": [
-        "npx nx run express-app:build:production"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        ".next/cache",
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "node dist/apps/express-app/main.js"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_pnpm.snap b/tests/snapshots/generate_plan_tests__node_pnpm.snap
index 1a54a79..761e4ec 100644
--- a/tests/snapshots/generate_plan_tests__node_pnpm.snap
+++ b/tests/snapshots/generate_plan_tests__node_pnpm.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "pnpm run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "pnpm i --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "pnpm-6_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "pnpm-6_x"
       ],
-      "commands": [
-        "pnpm run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "pnpm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap b/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap
index 7759dd2..a5aeb32 100644
--- a/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap
+++ b/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "pnpm run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "pnpm i --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-14_x"
-        },
-        {
-          "name": "pnpm-6_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-14_x",
+        "pnpm-6_x"
       ],
-      "commands": [
-        "pnpm run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "pnpm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap b/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap
index 59acb3e..866f7cc 100644
--- a/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap
+++ b/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "pnpm run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "pnpm i --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "pnpm-7_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "pnpm-7_x"
       ],
-      "commands": [
-        "pnpm run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "pnpm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_puppeteer.snap b/tests/snapshots/generate_plan_tests__node_puppeteer.snap
index 12d5241..f83aaef 100644
--- a/tests/snapshots/generate_plan_tests__node_puppeteer.snap
+++ b/tests/snapshots/generate_plan_tests__node_puppeteer.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,18 +36,17 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
+      ],
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "aptPackages": [
+      "nixpkgsArchive": "[archive]",
+      "aptPkgs": [
         "libnss3",
         "libatk1.0-0",
         "libatk-bridge2.0-0",
@@ -51,18 +59,9 @@ expression: plan
         "libxshmfence1",
         "libglu1"
       ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "npm run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_variables.snap b/tests/snapshots/generate_plan_tests__node_variables.snap
index 90c719e..d8ba8ed 100644
--- a/tests/snapshots/generate_plan_tests__node_variables.snap
+++ b/tests/snapshots/generate_plan_tests__node_variables.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "node index.js"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_yarn.snap b/tests/snapshots/generate_plan_tests__node_yarn.snap
index bffdbca..eb78262 100644
--- a/tests/snapshots/generate_plan_tests__node_yarn.snap
+++ b/tests/snapshots/generate_plan_tests__node_yarn.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "yarn run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "yarn install --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "yarn-1_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "yarn-1_x"
       ],
-      "commands": [
-        "yarn run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "yarn run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_berry.snap b/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
index 7dce0a4..0838564 100644
--- a/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
+++ b/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "yarn run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "yarn set version berry && yarn install --check-cache"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "yarn-1_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "yarn-1_x"
       ],
-      "commands": [
-        "yarn run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "yarn run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap b/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap
index 0e10e24..a9a6dbb 100644
--- a/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap
+++ b/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "yarn install --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-14_x"
-        },
-        {
-          "name": "yarn-1_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-14_x",
+        "yarn-1_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "yarn run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap b/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap
index d0374cb..a0c2d2a 100644
--- a/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap
+++ b/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "yarn run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "yarn install --frozen-lockfile"
       ],
       "cacheDirectories": [
@@ -27,32 +39,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-14_x"
-        },
-        {
-          "name": "yarn-1_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-14_x",
+        "yarn-1_x"
       ],
-      "commands": [
-        "yarn run build"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "yarn run start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap b/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap
index 99651be..63a125a 100644
--- a/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap
+++ b/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,22 @@ expression: plan
     "NODE_ENV": "test",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,29 +36,19 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "node index.js"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__php_laravel.snap b/tests/snapshots/generate_plan_tests__php_laravel.snap
index e762929..9b09fd2 100644
--- a/tests/snapshots/generate_plan_tests__php_laravel.snap
+++ b/tests/snapshots/generate_plan_tests__php_laravel.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "IS_LARAVEL": "yes",
@@ -15,53 +15,44 @@ expression: plan
     "php-fpm.conf": "[www]\nlisten = 127.0.0.1:9000\nuser = nobody\npm = dynamic\npm.max_children = 50\npm.min_spare_servers = 4\npm.max_spare_servers = 32\npm.start_servers = 18\nclear_env = no\n",
     "transform-config.pl": "#!/usr/bin/env perl\n\nundef $/;\n\nsub if_stmt {\n    my $condition = $_[0];\n    my $value = $_[1];\n    my $else = $_[2];\n\n    if($ENV{$condition} eq \"yes\") {\n        return $value;\n    } else {\n        return $else;\n    }\n}\n\nsub get_nix_path {\n    my $exe = $_[0];\n    my $path = `which $exe`;\n    $path =~ s/\\n//;\n    my $storePath = `nix-store -q $path`;\n    $storePath =~ s/\\n//;\n    return $storePath;\n}\n\nif ($#ARGV != 1) {\n    print STDERR \"Usage: $0 <config-file> <output-file>\\n\";\n    exit 1;\n}\nmy $infile = $ARGV[0];\nmy $outfile = $ARGV[1];\nopen(FH, '<', $infile) or die \"Could not open configuration file '$infile' $!\";\nmy $out = '';\nwhile (<FH>) {\n\n    # If statements\n    s{\n        \\$if\\s*\\((\\w+)\\)\\s*\\(\n            ([\\s\\S]*?)\n        \\)\\s*else\\s*\\(\n            ([\\s\\S]*?)\n        \\)\n    }{if_stmt($1, $2, $3)}egx;\n\n    # Variables\n    s/\\$\\{(\\w+)\\}/$ENV{$1}/eg;\n    \n    # Nix paths\n    s/\\$\\!\\{(\\w+)\\}/get_nix_path($1)/eg;\n\n    $out .= $_;\n}\nclose(FH);\nopen(FH, '>', $outfile) or die \"Could not write configuration file '$outfile' $!\";\nprint FH $out;\nclose(FH);"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "npm run prod"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx",
         "composer install",
         "npm i"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "php81"
-        },
-        {
-          "name": "perl"
-        },
-        {
-          "name": "nginx"
-        },
-        {
-          "name": "php81Packages.composer"
-        },
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "php81",
+        "perl",
+        "nginx",
+        "php81Packages.composer",
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "commands": [
-        "npm run prod"
-      ]
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "([ -e /app/storage ] && chmod -R ugo+w /app/storage); perl /assets/transform-config.pl /assets/nginx.template.conf /nginx.conf && echo \"Server starting on port $PORT\" && (php-fpm -y /assets/php-fpm.conf & nginx -c /nginx.conf)"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__php_vanilla.snap b/tests/snapshots/generate_plan_tests__php_vanilla.snap
index a852da9..b5d5116 100644
--- a/tests/snapshots/generate_plan_tests__php_vanilla.snap
+++ b/tests/snapshots/generate_plan_tests__php_vanilla.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "php",
@@ -14,41 +14,29 @@ expression: plan
     "php-fpm.conf": "[www]\nlisten = 127.0.0.1:9000\nuser = nobody\npm = dynamic\npm.max_children = 50\npm.min_spare_servers = 4\npm.max_spare_servers = 32\npm.start_servers = 18\nclear_env = no\n",
     "transform-config.pl": "#!/usr/bin/env perl\n\nundef $/;\n\nsub if_stmt {\n    my $condition = $_[0];\n    my $value = $_[1];\n    my $else = $_[2];\n\n    if($ENV{$condition} eq \"yes\") {\n        return $value;\n    } else {\n        return $else;\n    }\n}\n\nsub get_nix_path {\n    my $exe = $_[0];\n    my $path = `which $exe`;\n    $path =~ s/\\n//;\n    my $storePath = `nix-store -q $path`;\n    $storePath =~ s/\\n//;\n    return $storePath;\n}\n\nif ($#ARGV != 1) {\n    print STDERR \"Usage: $0 <config-file> <output-file>\\n\";\n    exit 1;\n}\nmy $infile = $ARGV[0];\nmy $outfile = $ARGV[1];\nopen(FH, '<', $infile) or die \"Could not open configuration file '$infile' $!\";\nmy $out = '';\nwhile (<FH>) {\n\n    # If statements\n    s{\n        \\$if\\s*\\((\\w+)\\)\\s*\\(\n            ([\\s\\S]*?)\n        \\)\\s*else\\s*\\(\n            ([\\s\\S]*?)\n        \\)\n    }{if_stmt($1, $2, $3)}egx;\n\n    # Variables\n    s/\\$\\{(\\w+)\\}/$ENV{$1}/eg;\n    \n    # Nix paths\n    s/\\$\\!\\{(\\w+)\\}/get_nix_path($1)/eg;\n\n    $out .= $_;\n}\nclose(FH);\nopen(FH, '>', $outfile) or die \"Could not write configuration file '$outfile' $!\";\nprint FH $out;\nclose(FH);"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "php"
-        },
-        {
-          "name": "perl"
-        },
-        {
-          "name": "nginx"
-        },
-        {
-          "name": "phpPackages.composer"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "php",
+        "perl",
+        "nginx",
+        "phpPackages.composer"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "([ -e /app/storage ] && chmod -R ugo+w /app/storage); perl /assets/transform-config.pl /assets/nginx.template.conf /nginx.conf && echo \"Server starting on port $PORT\" && (php-fpm -y /assets/php-fpm.conf & nginx -c /nginx.conf)"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__pin_archive.snap b/tests/snapshots/generate_plan_tests__pin_archive.snap
index 87e7385..026e36d 100644
--- a/tests/snapshots/generate_plan_tests__pin_archive.snap
+++ b/tests/snapshots/generate_plan_tests__pin_archive.snap
@@ -3,30 +3,17 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_START_CMD": "./start.sh"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixpacksArchive": "[archive]"
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+  "phases": {
+    "setup": {
+      "name": "setup"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./start.sh"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__procfile.snap b/tests/snapshots/generate_plan_tests__procfile.snap
index 90c719e..3c7d6ff 100644
--- a/tests/snapshots/generate_plan_tests__procfile.snap
+++ b/tests/snapshots/generate_plan_tests__procfile.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "CI": "true",
@@ -11,13 +11,25 @@ expression: plan
     "NODE_ENV": "production",
     "NPM_CONFIG_PRODUCTION": "false"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "npm run build"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm i"
       ],
       "cacheDirectories": [
@@ -27,29 +39,30 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "build",
+    "release": {
+      "name": "release",
       "dependsOn": [
-        "install"
+        "setup",
+        "install",
+        "build"
       ],
-      "cacheDirectories": [
-        "node_modules/.cache"
+      "cmds": [
+        "echo release"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
+      ],
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
-    "cmd": "node index.js"
+  },
+  "start": {
+    "cmd": "echo start from procfile"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python.snap b/tests/snapshots/generate_plan_tests__python.snap
index c810908..b38b6e2 100644
--- a/tests/snapshots/generate_plan_tests__python.snap
+++ b/tests/snapshots/generate_plan_tests__python.snap
@@ -3,34 +3,19 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
       ],
       "cacheDirectories": [
@@ -40,14 +25,21 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python main.py"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_2.snap b/tests/snapshots/generate_plan_tests__python_2.snap
index 3d47001..aad0627 100644
--- a/tests/snapshots/generate_plan_tests__python_2.snap
+++ b/tests/snapshots/generate_plan_tests__python_2.snap
@@ -3,42 +3,34 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python27"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python27",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python main.py"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_2_runtime.snap b/tests/snapshots/generate_plan_tests__python_2_runtime.snap
index 3d47001..aad0627 100644
--- a/tests/snapshots/generate_plan_tests__python_2_runtime.snap
+++ b/tests/snapshots/generate_plan_tests__python_2_runtime.snap
@@ -3,42 +3,34 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python27"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python27",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python main.py"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_django.snap b/tests/snapshots/generate_plan_tests__python_django.snap
index 282114b..ff1afcf 100644
--- a/tests/snapshots/generate_plan_tests__python_django.snap
+++ b/tests/snapshots/generate_plan_tests__python_django.snap
@@ -3,37 +3,19 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python,django,postgres",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "postgresql"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
       ],
       "cacheDirectories": [
@@ -43,14 +25,22 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "postgresql",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python manage.py migrate && gunicorn mysite.wsgi"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_django_mysql.snap b/tests/snapshots/generate_plan_tests__python_django_mysql.snap
index 8ef6fa5..5b85a16 100644
--- a/tests/snapshots/generate_plan_tests__python_django_mysql.snap
+++ b/tests/snapshots/generate_plan_tests__python_django_mysql.snap
@@ -3,37 +3,19 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python,django",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "libmysqlclient.dev"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
       ],
       "cacheDirectories": [
@@ -43,14 +25,22 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "libmysqlclient.dev",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python manage.py migrate && gunicorn mysite.wsgi"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_numpy.snap b/tests/snapshots/generate_plan_tests__python_numpy.snap
index c810908..b38b6e2 100644
--- a/tests/snapshots/generate_plan_tests__python_numpy.snap
+++ b/tests/snapshots/generate_plan_tests__python_numpy.snap
@@ -3,34 +3,19 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
       ],
       "cacheDirectories": [
@@ -40,14 +25,21 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python main.py"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_poetry.snap b/tests/snapshots/generate_plan_tests__python_poetry.snap
index 3318e3c..3458b9d 100644
--- a/tests/snapshots/generate_plan_tests__python_poetry.snap
+++ b/tests/snapshots/generate_plan_tests__python_poetry.snap
@@ -3,35 +3,20 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python,poetry",
     "NIXPACKS_POETRY_VERSION": "1.1.13",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install poetry==$NIXPACKS_POETRY_VERSION && poetry install --no-dev --no-interaction --no-ansi"
       ],
       "cacheDirectories": [
@@ -41,14 +26,21 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python main.py"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_procfile.snap b/tests/snapshots/generate_plan_tests__python_procfile.snap
index f464157..513bafe 100644
--- a/tests/snapshots/generate_plan_tests__python_procfile.snap
+++ b/tests/snapshots/generate_plan_tests__python_procfile.snap
@@ -3,34 +3,19 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
       ],
       "cacheDirectories": [
@@ -40,14 +25,21 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python src/main.py"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__python_setuptools.snap b/tests/snapshots/generate_plan_tests__python_setuptools.snap
index 23a0fa4..d9c9326 100644
--- a/tests/snapshots/generate_plan_tests__python_setuptools.snap
+++ b/tests/snapshots/generate_plan_tests__python_setuptools.snap
@@ -3,34 +3,19 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "python",
     "PYTHONUNBUFFERED": "1"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "python38"
-        },
-        {
-          "name": "gcc"
-        }
-      ],
-      "nixLibraries": [
-        "zlib",
-        "stdenv.cc.cc.lib"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install --upgrade build setuptools && pip install ."
       ],
       "onlyIncludeFiles": [
@@ -43,14 +28,21 @@ expression: plan
         "/opt/venv/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python38",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "python -m nixpacks-setuptools"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__ruby.snap b/tests/snapshots/generate_plan_tests__ruby.snap
index a1290c6..0f59156 100644
--- a/tests/snapshots/generate_plan_tests__ruby.snap
+++ b/tests/snapshots/generate_plan_tests__ruby.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "BUNDLE_GEMFILE": "/app/Gemfile",
@@ -11,26 +11,13 @@ expression: plan
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
     "NIXPACKS_METADATA": "Ruby"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "aptPackages": [
-        "procps"
-      ],
-      "commands": [
-        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
-        "rvm install ruby-3.1.2",
-        "rvm --default use ruby-3.1.2",
-        "gem install bundler:2.3.7",
-        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bundle install"
       ],
       "cacheDirectories": [
@@ -42,14 +29,21 @@ expression: plan
         "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+    "setup": {
+      "name": "setup",
+      "aptPkgs": [
+        "procps"
+      ],
+      "cmds": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+        "rvm install ruby-3.1.2",
+        "rvm --default use ruby-3.1.2",
+        "gem install bundler:2.3.7",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
       ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "ruby app.rb"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__ruby_local_deps.snap b/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
index 5fbb7c3..3d222c8 100644
--- a/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "BUNDLE_GEMFILE": "/app/Gemfile",
@@ -11,26 +11,13 @@ expression: plan
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
     "NIXPACKS_METADATA": "Ruby"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "aptPackages": [
-        "procps"
-      ],
-      "commands": [
-        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
-        "rvm install ruby-3.1.2",
-        "rvm --default use ruby-3.1.2",
-        "gem install bundler:2.3.7",
-        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bundle install"
       ],
       "cacheDirectories": [
@@ -42,14 +29,21 @@ expression: plan
         "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+    "setup": {
+      "name": "setup",
+      "aptPkgs": [
+        "procps"
+      ],
+      "cmds": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+        "rvm install ruby-3.1.2",
+        "rvm --default use ruby-3.1.2",
+        "gem install bundler:2.3.7",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
       ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "bundle exec ruby app.rb"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
index 63119a7..1314f36 100644
--- a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "BUNDLE_GEMFILE": "/app/Gemfile",
@@ -11,13 +11,22 @@ expression: plan
     "GEM_PATH": "/usr/local/rvm/gems/3.1.2:/usr/local/rvm/gems/3.1.2@global",
     "NIXPACKS_METADATA": "Ruby"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "bundle exec rake assets:precompile"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bundle install"
       ],
       "cacheDirectories": [
@@ -29,31 +38,22 @@ expression: plan
         "/usr/local/rvm/gems/3.1.2@global/bin"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "aptPackages": [
+      "aptPkgs": [
         "procps",
         "libpq-dev"
       ],
-      "commands": [
+      "cmds": [
         "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
         "rvm install 3.1.2",
         "rvm --default use 3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
       ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ],
-      "commands": [
-        "bundle exec rake assets:precompile"
-      ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "rake db:migrate && bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000}"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
index 70dee71..73a4dd1 100644
--- a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "BUNDLE_GEMFILE": "/app/Gemfile",
@@ -11,26 +11,13 @@ expression: plan
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
     "NIXPACKS_METADATA": "Ruby"
   },
-  "phases": [
-    {
-      "name": "setup",
-      "aptPackages": [
-        "procps"
-      ],
-      "commands": [
-        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
-        "rvm install ruby-3.1.2",
-        "rvm --default use ruby-3.1.2",
-        "gem install bundler:2.3.7",
-        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
-    },
-    {
+  "phases": {
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "bundle install"
       ],
       "cacheDirectories": [
@@ -42,14 +29,21 @@ expression: plan
         "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
       ]
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+    "setup": {
+      "name": "setup",
+      "aptPkgs": [
+        "procps"
+      ],
+      "cmds": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+        "rvm install ruby-3.1.2",
+        "rvm --default use ruby-3.1.2",
+        "gem install bundler:2.3.7",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
       ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "RACK_ENV=production bundle exec puma"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__ruby_with_node.snap b/tests/snapshots/generate_plan_tests__ruby_with_node.snap
index ecccb29..c96f5af 100644
--- a/tests/snapshots/generate_plan_tests__ruby_with_node.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_with_node.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "BUNDLE_GEMFILE": "/app/Gemfile",
@@ -11,38 +11,30 @@ expression: plan
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
     "NIXPACKS_METADATA": "Ruby"
   },
-  "phases": [
-    {
-      "name": "node:setup",
-      "nixPackages": [
-        {
-          "name": "nodejs-16_x"
-        },
-        {
-          "name": "npm-8_x",
-          "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
-        }
-      ]
-    },
-    {
-      "name": "setup",
-      "aptPackages": [
-        "procps"
+  "phases": {
+    "install": {
+      "name": "install",
+      "dependsOn": [
+        "setup"
       ],
-      "commands": [
-        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
-        "rvm install ruby-3.1.2",
-        "rvm --default use ruby-3.1.2",
-        "gem install bundler:2.3.7",
-        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+      "cmds": [
+        "bundle install"
+      ],
+      "cacheDirectories": [
+        "/root/.bundle/cache"
+      ],
+      "paths": [
+        "/usr/local/rvm/rubies/ruby-3.1.2/bin",
+        "/usr/local/rvm/gems/ruby-3.1.2/bin",
+        "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
       ]
     },
-    {
+    "node:install": {
       "name": "node:install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "npm ci"
       ],
       "cacheDirectories": [
@@ -52,31 +44,32 @@ expression: plan
         "/app/node_modules/.bin"
       ]
     },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
+    "node:setup": {
+      "name": "node:setup",
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
       ],
-      "commands": [
-        "bundle install"
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "cacheDirectories": [
-        "/root/.bundle/cache"
-      ],
-      "paths": [
-        "/usr/local/rvm/rubies/ruby-3.1.2/bin",
-        "/usr/local/rvm/gems/ruby-3.1.2/bin",
-        "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
-      ]
+      "nixpkgsArchive": "[archive]"
     },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+    "setup": {
+      "name": "setup",
+      "aptPkgs": [
+        "procps"
+      ],
+      "cmds": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+        "rvm install ruby-3.1.2",
+        "rvm --default use ruby-3.1.2",
+        "gem install bundler:2.3.7",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
       ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "ruby app.rb"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap b/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap
index c27fc55..39b8035 100644
--- a/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap
+++ b/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap
@@ -3,41 +3,20 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "rust",
     "NIXPACKS_NO_MUSL": "1",
     "ROCKET_ADDRESS": "0.0.0.0"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "gcc"
-        },
-        {
-          "name": "rust-bin.stable.latest.default",
-          "overlay": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
-        }
-      ],
-      "aptPackages": [
-        "binutils"
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
-        "install"
+        "setup"
       ],
-      "commands": [
+      "cmds": [
         "mkdir -p bin",
         "cargo build --release",
         "cp target/release/rocket bin"
@@ -47,9 +26,23 @@ expression: plan
         "/root/.cargo/registry",
         "target"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "gcc",
+        "rust-bin.stable.latest.default"
+      ],
+      "nixOverlays": [
+        "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]",
+      "aptPkgs": [
+        "binutils"
+      ]
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./bin/rocket"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__shell_hello.snap b/tests/snapshots/generate_plan_tests__shell_hello.snap
index d40a66e..ae752fb 100644
--- a/tests/snapshots/generate_plan_tests__shell_hello.snap
+++ b/tests/snapshots/generate_plan_tests__shell_hello.snap
@@ -3,29 +3,21 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_START_CMD": "./start.sh"
   },
-  "phases": [
-    {
-      "name": "setup"
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ]
+  "phases": {
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "cowsay"
+      ],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./start.sh"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__staticfile.snap b/tests/snapshots/generate_plan_tests__staticfile.snap
index d675737..6a04747 100644
--- a/tests/snapshots/generate_plan_tests__staticfile.snap
+++ b/tests/snapshots/generate_plan_tests__staticfile.snap
@@ -3,7 +3,7 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "staticfile"
@@ -12,32 +12,26 @@ expression: plan
     ".htpasswd": "# Username: nixpacks\n# Password: nixpacks\nnixpacks:$apr1$21i3ye0o$d1H1ePSg8mD2cO5O2A9dG.",
     "nginx.conf": "daemon off;\nerror_log /dev/stdout info;\nworker_processes  auto;\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include /nix/store/*-user-environment/conf/mime.types;\n    access_log /dev/stdout;\n    default_type  application/octet-stream;\n    sendfile       on;\n    keepalive_timeout  60;\n    types_hash_max_size 4096;\n    server {\n        listen    0.0.0.0:80;\n        gzip  \t  on;\n        root\t  /app/site;\n        location / {\n            auth_basic\t\"Password Required\";\nauth_basic_user_file\t/assets/.htpasswd;\n            autoindex off;\n        }\n\n    }\n}\n"
   },
-  "phases": [
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ]
-    },
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "nginx"
-        }
-      ]
-    },
-    {
+  "phases": {
+    "build": {
       "name": "build",
       "dependsOn": [
         "install"
       ],
-      "commands": [
+      "cmds": [
         "mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/"
       ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "nginx"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\" /assets/nginx.conf && nginx -c /assets/nginx.conf"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__swift.snap b/tests/snapshots/generate_plan_tests__swift.snap
index 3ab9205..cc59f6b 100644
--- a/tests/snapshots/generate_plan_tests__swift.snap
+++ b/tests/snapshots/generate_plan_tests__swift.snap
@@ -3,57 +3,48 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "swift"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "CC=clang++ swift build -c release --static-swift-stdlib",
+        "cp ./.build/release/swift ./swift && rm -rf ./.build"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "swift package resolve"
       ],
       "onlyIncludeFiles": [
         "Package.swift"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "coreutils"
-        },
-        {
-          "name": "swift"
-        },
-        {
-          "name": "clang"
-        },
-        {
-          "name": "zlib"
-        },
-        {
-          "name": "zlib.dev"
-        }
+      "nixPkgs": [
+        "coreutils",
+        "swift",
+        "clang",
+        "zlib",
+        "zlib.dev"
       ],
-      "nixpacksArchive": "[archive]"
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ],
-      "commands": [
-        "CC=clang++ swift build -c release --static-swift-stdlib",
-        "cp ./.build/release/swift ./swift && rm -rf ./.build"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./swift"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__swift_custom_version.snap b/tests/snapshots/generate_plan_tests__swift_custom_version.snap
index 3ab9205..cc59f6b 100644
--- a/tests/snapshots/generate_plan_tests__swift_custom_version.snap
+++ b/tests/snapshots/generate_plan_tests__swift_custom_version.snap
@@ -3,57 +3,48 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "swift"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "CC=clang++ swift build -c release --static-swift-stdlib",
+        "cp ./.build/release/swift ./swift && rm -rf ./.build"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "swift package resolve"
       ],
       "onlyIncludeFiles": [
         "Package.swift"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "coreutils"
-        },
-        {
-          "name": "swift"
-        },
-        {
-          "name": "clang"
-        },
-        {
-          "name": "zlib"
-        },
-        {
-          "name": "zlib.dev"
-        }
+      "nixPkgs": [
+        "coreutils",
+        "swift",
+        "clang",
+        "zlib",
+        "zlib.dev"
       ],
-      "nixpacksArchive": "[archive]"
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ],
-      "commands": [
-        "CC=clang++ swift build -c release --static-swift-stdlib",
-        "cp ./.build/release/swift ./swift && rm -rf ./.build"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./swift"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__swift_vapor.snap b/tests/snapshots/generate_plan_tests__swift_vapor.snap
index 657463b..b58fbbf 100644
--- a/tests/snapshots/generate_plan_tests__swift_vapor.snap
+++ b/tests/snapshots/generate_plan_tests__swift_vapor.snap
@@ -3,18 +3,28 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "swift"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "CC=clang++ swift build -c release --static-swift-stdlib",
+        "cp ./.build/release/Run ./Run && rm -rf ./.build"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ],
-      "commands": [
+      "cmds": [
         "swift package resolve"
       ],
       "onlyIncludeFiles": [
@@ -22,39 +32,20 @@ expression: plan
         "Package.resolved"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "coreutils"
-        },
-        {
-          "name": "swift"
-        },
-        {
-          "name": "clang"
-        },
-        {
-          "name": "zlib"
-        },
-        {
-          "name": "zlib.dev"
-        }
+      "nixPkgs": [
+        "coreutils",
+        "swift",
+        "clang",
+        "zlib",
+        "zlib.dev"
       ],
-      "nixpacksArchive": "[archive]"
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
-      ],
-      "commands": [
-        "CC=clang++ swift build -c release --static-swift-stdlib",
-        "cp ./.build/release/Run ./Run && rm -rf ./.build"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./Run"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__zig.snap b/tests/snapshots/generate_plan_tests__zig.snap
index 643199d..a78911d 100644
--- a/tests/snapshots/generate_plan_tests__zig.snap
+++ b/tests/snapshots/generate_plan_tests__zig.snap
@@ -3,37 +3,37 @@ source: tests/generate_plan_tests.rs
 expression: plan
 ---
 {
-  "nixpacksVersion": "[version]",
+  "providers": [],
   "buildImage": "[build_image]",
   "variables": {
     "NIXPACKS_METADATA": "zig"
   },
-  "phases": [
-    {
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cmds": [
+        "zig build -Drelease-safe=true"
+      ]
+    },
+    "install": {
       "name": "install",
       "dependsOn": [
         "setup"
       ]
     },
-    {
+    "setup": {
       "name": "setup",
-      "nixPackages": [
-        {
-          "name": "zig"
-        }
-      ]
-    },
-    {
-      "name": "build",
-      "dependsOn": [
-        "install"
+      "nixPkgs": [
+        "zig"
       ],
-      "commands": [
-        "zig build -Drelease-safe=true"
-      ]
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
     }
-  ],
-  "startPhase": {
+  },
+  "start": {
     "cmd": "./zig-out/bin/zig"
   }
 }