SfSkills Domains Docs GitHub

Home / devops / salesforce-dx-project-structure

salesforce-dx-project-structure

Use this skill when setting up, configuring, or troubleshooting sfdx-project.json, organizing packageDirectories for mono-repo or multi-package projects, managing sourceApiVersion alignment, and structuring the standard source-format directory tree. NOT for scratch org definition files (use devops/scratch-org-management) or CLI command usage — use apex/sf-cli-and-sfdx-essentials.

Open the skill folder on GitHub


Salesforce DX Project Structure

This skill activates when you need to create, configure, or troubleshoot the root project configuration file (sfdx-project.json) and directory layout for a Salesforce DX project. It covers packageDirectories configuration, sourceApiVersion management, multi-package mono-repo organization, namespace handling, and the standard source-format directory tree.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Understanding DX project structure requires clarity on four areas: the project configuration file, package directories, source format layout, and API version alignment.

sfdx-project.json

The sfdx-project.json file is the root configuration file for every Salesforce DX project. It lives at the repository root and is required for all sf CLI operations. The file must contain at minimum a packageDirectories array with at least one entry. Other top-level keys include sourceApiVersion, namespace, sfdcLoginUrl, signupTargetLoginUrl, and plugins.

The CLI resolves the project root by walking up the directory tree until it finds this file. If the file is missing or malformed, every sf command fails immediately.

packageDirectories

The packageDirectories array defines which folders contain deployable metadata. Each entry requires a path (relative to the project root). Exactly one entry must have "default": true — this is where sf CLI commands place newly created metadata by default.

For unlocked or managed packages, each entry can also include package (the package name), versionName, versionNumber (format: MAJOR.MINOR.PATCH.NEXT), and dependencies (array of package references with minimum version constraints).

Source Format Directory Layout

Inside each package directory, metadata follows the Salesforce source format tree. The standard structure is:

force-app/
  main/
    default/
      classes/
      triggers/
      lwc/
      aura/
      objects/
        Account/
          fields/
          listViews/
      flows/
      permissionsets/
      profiles/
      layouts/
      tabs/
      applications/
      staticresources/

Each metadata type has a defined folder name and file extension. Objects decompose into subfolders for fields, list views, record types, and other child components. This decomposed structure enables granular source tracking and conflict-free merging.

sourceApiVersion

The sourceApiVersion is a string (e.g., "62.0") that tells the CLI which metadata API version to use for deploys and retrieves. If this version is higher than the org's API version, the deployment fails with UNSUPPORTED_API_VERSION. If it is too low, newer metadata types are silently excluded from retrieves. Best practice: set it to the current API version of your lowest-supported org.


Common Patterns

Single-Package Project

When to use: Small to medium projects with one team deploying from one directory.

How it works:

{
  "packageDirectories": [
    {
      "path": "force-app",
      "default": true
    }
  ],
  "namespace": "",
  "sourceApiVersion": "62.0",
  "sfdcLoginUrl": "https://login.salesforce.com"
}

All metadata lives under force-app/main/default/. The sf CLI generates new components here automatically. This is the simplest and most common layout.

Why not multi-package: Adding unnecessary package boundaries creates build complexity, dependency management overhead, and longer CI times with no benefit for a single-team project.

Multi-Package Mono-Repo

When to use: Large projects where multiple teams own separate packages, or where you need independent package versioning and deployment ordering.

How it works:

{
  "packageDirectories": [
    {
      "path": "packages/core",
      "default": true,
      "package": "CoreDataModel",
      "versionName": "Spring 25",
      "versionNumber": "1.3.0.NEXT"
    },
    {
      "path": "packages/sales",
      "package": "SalesAutomation",
      "versionName": "Spring 25",
      "versionNumber": "2.1.0.NEXT",
      "dependencies": [
        {
          "package": "CoreDataModel",
          "versionNumber": "1.3.0.LATEST"
        }
      ]
    },
    {
      "path": "packages/service",
      "package": "ServiceExtensions",
      "versionName": "Spring 25",
      "versionNumber": "1.0.0.NEXT",
      "dependencies": [
        {
          "package": "CoreDataModel",
          "versionNumber": "1.3.0.LATEST"
        }
      ]
    }
  ],
  "namespace": "",
  "sourceApiVersion": "62.0"
}

Each team owns its directory. Dependencies flow upward (sales and service depend on core). Package version creation builds in dependency order automatically.

Why not a single directory: With multiple teams, a flat directory causes merge conflicts, unclear ownership boundaries, and makes independent deployment impossible.

Unpackaged Metadata Alongside Packages

When to use: When you have packaged components but also need org-specific configuration (profiles, permission sets, page layouts) that should not be packaged.

How it works:

{
  "packageDirectories": [
    {
      "path": "packages/core",
      "default": true,
      "package": "CorePackage",
      "versionName": "v1",
      "versionNumber": "1.0.0.NEXT"
    },
    {
      "path": "unpackaged",
      "default": false
    }
  ],
  "namespace": "",
  "sourceApiVersion": "62.0"
}

The unpackaged/ directory holds metadata that deploys via sf project deploy but is never included in package version builds. This is the correct place for profiles, org-specific permission sets, and post-install configuration.


Decision Guidance

SituationRecommended ApproachReason
Single team, no package versioning neededSingle force-app directory, no package keySimplest setup; avoids package build overhead
Multiple teams sharing one repoMulti-package with explicit dependenciesClear ownership; independent versioning; dependency-ordered builds
Managed package developmentSingle package dir with namespace set at project levelNamespace applies to all metadata in the project
Mix of packaged and org-specific metadataSeparate unpackaged/ directory entry without package keyKeeps org config out of package versions
API version mismatch errors on deployLower sourceApiVersion to match oldest target orgPrevents UNSUPPORTED_API_VERSION rejections
Need to support ISV with managed + extension packagesMulti-package with namespace on managed, blank on extensionsEach package can have its own lifecycle

Step-by-step instructions for setting up or restructuring a Salesforce DX project:

  1. Check for existing configuration — Look for sfdx-project.json at the repo root. If it exists, read it and identify what needs to change. If it does not exist, run sf project generate --name <project-name> to scaffold.
  2. Determine package strategy — Decide whether the project needs a single package directory, multiple packages, or a mix of packaged and unpackaged metadata. Base this on team count, deployment independence requirements, and ISV packaging needs.
  3. Configure packageDirectories — Add entries for each directory. Ensure exactly one has "default": true. For package development, add package, versionName, and versionNumber fields. Wire dependencies between packages in the correct order (base packages have no dependencies; downstream packages reference upstream versions).
  4. Set sourceApiVersion — Set to the current API version of your lowest-supported production org. Verify by running sf org display --target-org <alias> and checking the API version in the output.
  5. Create the directory tree — Build out the source-format folder structure inside each package directory. Follow the standard layout: <path>/main/default/<metadataType>/. Run sf project retrieve start to pull existing metadata into the correct structure.
  6. Validate the configuration — Run sf project deploy start --dry-run --target-org <alias> to verify the project structure deploys cleanly. Check for path resolution errors, missing metadata types, and API version mismatches.
  7. Commit and verify — Ensure sfdx-project.json is committed. Never commit sfdxAuthUrl values, .sfdx/ local state, or sandbox credentials. Add .sfdx/, .sf/, and auth files to .gitignore.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. UNSUPPORTED_API_VERSION on deploy — If sourceApiVersion in sfdx-project.json is higher than the target org's current API version, the deploy is rejected outright. This commonly happens when a developer updates the version after a Salesforce release but the target sandbox has not been refreshed to the new release yet.
  2. Missing default packageDirectory — If no entry has "default": true, the CLI cannot resolve where to place newly generated metadata. Commands like sf apex generate class fail with a cryptic path resolution error rather than a clear "no default directory" message.
  3. Path case sensitivity across OS — macOS is case-insensitive by default but Linux CI runners are case-sensitive. A path of Force-App works locally but breaks in CI if the actual directory is force-app. This causes intermittent "package directory not found" failures that only appear in pipelines.
  4. Namespace is project-wide, not per-directory — The namespace key applies to the entire project. You cannot assign different namespaces to different packageDirectories within the same sfdx-project.json. Multi-namespace development requires separate repositories.
  5. Duplicate path entries silently ignored — If two packageDirectories point to the same path, the CLI uses the first entry and silently ignores the second. This can mask configuration errors where a developer intended separate packages but used the same path.

Output Artifacts

ArtifactDescription
sfdx-project.jsonRoot project configuration file with packageDirectories, sourceApiVersion, and optional package metadata
Directory layoutSource-format folder tree under each package directory path
.gitignore additionsEntries for .sfdx/, .sf/, and auth files to prevent credential leaks

Package files