Modern Salesforce source control works like this: you store your org's metadata as source-format files inside a local Salesforce DX project, edit them in Visual Studio Code with the Salesforce Extension Pack, version every change in Git and GitHub, and let the unified sf command-line interface (CLI v2) retrieve from and deploy to your orgs — while a CI pipeline validates each pull request before it ever reaches production. Manual change sets and copy-paste are replaced by a repeatable, reviewable, automated workflow.
This guide covers the genuinely current 2026 toolchain. The old standalone sfdx executable and force:source:* commands are retired, so everything below uses the sf command (legacy commands are noted only as a "formerly").
Key takeaways
- Salesforce DX stores metadata as source format in a Git repository — not the old metadata-API zip — so diffs, code review, and merges actually work.
- The unified
sfCLI (npm package@salesforce/cli, v2) replaces the deprecatedsfdxexecutable;force:source:push/force:source:pullare nowsf project deploy start/sf project retrieve start. - VS Code plus the Salesforce Extension Pack give you org browsing, Apex IntelliSense, debugging, and one-click retrieve/deploy.
- Scratch orgs spun up from a Dev Hub are disposable, source-tracked dev environments; sandboxes stay for integration and UAT.
- Ship work as unlocked packages for versioned, modular deployment instead of monolithic change sets.
- A GitHub Actions pipeline that runs
sf project deploy validateon every PR catches failures before they hit production.
What is source-driven Salesforce development?
Source-driven (or "org-as-source-of-truth" turned around) development treats your Git repository as the source of truth, not the org. Instead of clicking through Setup and pushing change sets between environments, you describe the org as code, review changes through pull requests, and deploy them through automation.
Two formats matter here:
- Source format — the decomposed, human-readable layout Salesforce DX uses under
force-app/main/default/. Large metadata (custom objects, fields, list views) is split into small files, which makes Git diffs and merges sane. - Metadata API format — the older single-folder,
package.xml-driven zip format. The CLI still converts to and from it (sf project convert source), but you author in source format.
If you are formalizing your team's workflow, our guide on techniques and tools to improve the Salesforce development cycle pairs well with everything below.
Org Development vs Package Development: which model fits?
Salesforce DX supports two project models. Pick based on whether you deploy to one known org or distribute reusable, versioned modules.
| Aspect | Org Development model | Package Development model |
|---|---|---|
| Source of truth | Git repo mirroring one production org | Git repo of independent, versioned packages |
| Best for | A single org / classic CRM customization | Modular apps, ISVs, large multi-team orgs |
| Deploy unit | The force-app directory (metadata) |
An unlocked or managed package version |
| Environments | Sandboxes + (optionally) scratch orgs | Scratch orgs for build, then install packages |
| Versioning | Git history of metadata | Git history plus package version numbers |
Choosing the package route? Read what the different types of packages in Salesforce are before you split a monolith.
Either model replaces change sets. Here is why source-driven deployment wins:
| Change sets (clicks) | Source-driven (sf CLI + Git) |
|
|---|---|---|
| Repeatable | No — rebuilt by hand each time | Yes — same commands, same manifest |
| Reviewable | No diff, no history | Full Git diff + pull request review |
| Automatable | Cannot run in CI | Runs in GitHub Actions / any CI |
| Rollback | Manual re-creation | git revert + redeploy |
| Tests | Run on deploy only | Validate on every PR before merge |
What do you need to install?
Three things, in order:
- Node.js (LTS) — the
sfCLI ships as an npm package, so install Node first. - Salesforce CLI (
sf, v2) — the unified CLI that talks to your orgs. - Visual Studio Code + Salesforce Extension Pack — your editor and org tooling.
Install the CLI with npm:
# Install the unified Salesforce CLI (the `sf` executable, v2)
npm install --global @salesforce/cli
# Verify the install and version
sf --version
# @salesforce/cli/2.x.x linux-x64 node-v20.x.x
# Update to the latest release whenever you like
sf update
# Note: the standalone "sfdx" installer and the "sfdx force:*" commands are
# retired. The sfdx executable now just forwards to sf — use sf for new work.# Install the Salesforce Extension Pack into VS Code from the command line
code --install-extension salesforce.salesforcedx-vscode
# (Or search "Salesforce Extension Pack" in the VS Code Extensions view —
# publisher id: salesforce.salesforcedx-vscode)
# Optional: the expanded pack adds SOQL, Apex Replay Debugger extras
code --install-extension salesforce.salesforcedx-vscode-expandedHow do you create a project and connect VS Code to your org?
Generate a Salesforce DX project, then authorize the orgs you will work against. In VS Code you can do the same from the Command Palette (Ctrl/Cmd+Shift+P → "SFDX: Create Project" and "SFDX: Authorize an Org"), but the CLI is what CI uses, so learn it directly.
# Create a new source-format DX project, then enter it
sf project generate --name my-sfdx-project
cd my-sfdx-project
# Resulting layout:
# force-app/main/default/ <- your source-format metadata lives here
# config/project-scratch-def.json
# manifest/package.xml <- create with: sf project generate manifest
# .forceignore <- metadata to skip on deploy/retrieve
# sfdx-project.json <- project + package config# Authorize a sandbox via the browser (web server flow)
sf org login web --alias my-sandbox --instance-url https://test.salesforce.com
# Authorize production / a developer org (default login URL)
sf org login web --alias prod --instance-url https://login.salesforce.com
# Authorize an org AND mark it as your Dev Hub (required for scratch orgs/packages)
sf org login web --alias DevHub --set-default-dev-hub
# List every authorized org and see which is the default
sf org list
# Formerly: sfdx force:auth:web:login -a my-sandboxHow do you retrieve and deploy metadata?
Retrieve pulls metadata from an org into your project; deploy pushes your local changes out. Scope either by manifest (package.xml) or by directory/type. Always validate before a real production deploy.
# Retrieve everything listed in your manifest into the project
sf project retrieve start --manifest manifest/package.xml --target-org my-sandbox
# Retrieve just one folder or metadata type
sf project retrieve start --source-dir force-app/main/default/classes
# Deploy your local changes to an org
sf project deploy start --source-dir force-app --target-org my-sandbox
# Validate-only: runs tests, commits NOTHING to the org (do this before prod)
sf project deploy validate --source-dir force-app --test-level RunLocalTests --target-org prod
# After a successful validation you can do a fast "quick deploy" of that job id
sf project deploy quick --job-id <validatedJobId> --target-org prod
# Formerly: sfdx force:source:deploy / sfdx force:source:retrieveHow do scratch orgs and source tracking work?
A scratch org is a disposable, configurable org created from your Dev Hub. It is source-tracked, so the CLI knows exactly what changed on either side. This is the modern replacement for clicking around a shared sandbox.
For sandboxes, scratch orgs track changes automatically; on org types that do not, enable it with sf org enable tracking.
# Create a Dev Hub-backed scratch org (disposable, source-tracked)
sf org create scratch \
--target-dev-hub DevHub \
--definition-file config/project-scratch-def.json \
--alias my-scratch --set-default --duration-days 7
# Push local source to the scratch org, and pull org-side changes back
sf project deploy start --target-org my-scratch # formerly: force:source:push
sf project retrieve start --target-org my-scratch # formerly: force:source:pull
# Preview what would sync before you run it
sf project deploy preview --target-org my-scratch
# Enable tracking on a sandbox that needs it
sf org enable tracking --target-org my-sandbox
# Open the scratch org in your browser, then delete it when done
sf org open --target-org my-scratch
sf org delete scratch --target-org my-scratchWhat are the manifest (package.xml) and .forceignore?
The manifest (manifest/package.xml) lists which metadata types and members to retrieve or deploy. Generate it from an org instead of hand-writing it:
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>*</members>
<name>ApexClass</name>
</types>
<types>
<members>*</members>
<name>CustomObject</name>
</types>
<types>
<members>Account-Account Layout</members>
<name>Layout</name>
</types>
<version>64.0</version>
</Package># Generate a manifest from an org's metadata (no hand-editing XML)
sf project generate manifest --from-org my-sandbox --metadata ApexClass,CustomObject
# Generate a manifest from local source you already have
sf project generate manifest --source-dir force-appThe .forceignore file tells the CLI which files to skip when deploying and retrieving — think .gitignore, but for metadata. Use it to keep noisy, org-owned, or environment-specific metadata out of your syncs.
# .forceignore — metadata the CLI should skip on deploy/retrieve
# Org-managed / environment-specific noise
**/profiles/*.profile-meta.xml
**/settings/*.settings-meta.xml
**/*.appMenu-meta.xml
# Generated or local-only files
**/jsconfig.json
**/.eslintrc.json
.localdevserverHow do you set up CI/CD with GitHub Actions?
The payoff of source control is automation. A minimal pipeline authenticates to an org with the JWT bearer flow (no interactive login) and runs sf project deploy validate on every pull request, so failing tests or metadata block the merge — not production.
# .github/workflows/validate.yml
name: Validate on PR
on:
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Salesforce CLI
run: npm install --global @salesforce/cli
- name: Authenticate to org (JWT bearer flow)
run: |
echo "${SF_JWT_KEY}" > server.key
sf org login jwt \
--client-id "${SF_CONSUMER_KEY}" \
--jwt-key-file server.key \
--username "${SF_USERNAME}" \
--instance-url https://login.salesforce.com \
--alias ci-org
env:
SF_JWT_KEY: ${{ secrets.SF_JWT_KEY }}
SF_CONSUMER_KEY: ${{ secrets.SF_CONSUMER_KEY }}
SF_USERNAME: ${{ secrets.SF_USERNAME }}
- name: Validate deployment (commits nothing to the org)
run: |
sf project deploy validate \
--source-dir force-app \
--test-level RunLocalTests \
--target-org ci-orgSource control best practices for Salesforce teams
- Branch per change. Feature branches + pull requests give you review and an audit trail; protect
mainso nothing merges without a green CI check. - One scratch org per feature. Build in a disposable org, then throw it away — no more "who changed my sandbox?"
- Keep
.forceignorehonest. Exclude org-managed and environment-specific metadata so deploys stay clean across environments. - Validate before you deploy. Run
sf project deploy validate --test-level RunLocalTestsin CI; promote withsf project deploy quick. - Package what you can. Unlocked packages make large orgs modular and versioned.
These mechanics only pay off on top of solid design — see our Salesforce customization best practices and the common pitfalls in how to avoid Salesforce software project failure.
Want this set up for your team — DX projects, scratch orgs, and CI/CD wired into GitHub? Our Salesforce consulting and development team can stand up a source-driven, automated release pipeline so every change is reviewed, tested, and deployed with confidence.
Frequently Asked Questions
Is sfdx deprecated, and should I switch to sf?
Yes. The standalone sfdx executable and the sfdx force:* command style are retired. The current tool is the unified Salesforce CLI v2, installed as the npm package @salesforce/cli and run as sf. The sfdx command now simply forwards to sf, and old commands map to new ones — for example force:source:push becomes sf project deploy start and force:source:pull becomes sf project retrieve start. Use sf for all new work.
What is the difference between source format and metadata API format?
Source format is the decomposed, human-readable layout that Salesforce DX uses under force-app/main/default/, where large metadata such as custom objects and fields is split into many small files so Git diffs and merges work well. Metadata API format is the older single-folder zip format driven entirely by package.xml. You author in source format; the CLI converts between the two with sf project convert source and sf project convert mdapi when needed.
Do I need a Dev Hub to use scratch orgs?
Yes. Scratch orgs are created from a Dev Hub, which is an org (production, Developer Edition, or a trial) where you enable the Dev Hub feature. You authorize it with sf org login web --set-default-dev-hub, then create scratch orgs with sf org create scratch. Sandboxes do not require a Dev Hub, but they are not disposable or as quickly reproducible as scratch orgs.
How do I deploy to production safely from VS Code or the CLI?
Never deploy straight to production from the editor without checks. Run a validation-only deploy first with sf project deploy validate --test-level RunLocalTests --target-org prod, which runs Apex tests and commits nothing. If it passes, promote the exact validated build with sf project deploy quick --job-id <id>. In practice this should run from CI on a protected branch, not a developer's laptop.
Change sets or source-driven deployment — which is better?
Source-driven deployment is better for any team that wants repeatability and review. Change sets are built by hand in the UI, have no diff or history, cannot run in CI, and are tedious to roll back. Source-driven deployment uses the sf CLI plus Git, so every change is versioned, reviewed in a pull request, validated automatically, and reproducible across environments. Change sets are only reasonable for tiny, one-off moves.
What is a .forceignore file used for?
The .forceignore file tells the Salesforce CLI which files to skip during deploy and retrieve operations — it is the metadata equivalent of .gitignore. Teams use it to exclude org-managed, generated, or environment-specific metadata (such as certain profiles, settings, or local tooling files) so that syncs stay clean and deployments do not fail or overwrite environment-specific configuration.