Salesforce DX Project Structure
A directory is a Salesforce DX project because it contains sfdx-project.json. That file tells the
CLI which directories hold source (packageDirectories), what metadata shape to speak
(sourceApiVersion), and where to log in (sfdcLoginUrl).
When to use
| Situation |
Action |
| Creating a repo or adding a package directory |
sfdx-project.json schema + multiple-package-directory rules below |
| "Where does this metadata file go?" |
references/metadata-directory-map.md |
| Source keeps getting deployed that should not be |
.forceignore precedence, pattern 4, and sf project list ignored |
| Building a deployment or deletion manifest |
sf project generate manifest, references/forceignore-and-manifests.md |
sf project deploy start reports conflicts |
Pattern 5 — resolve, then deploy or retrieve with --ignore-conflicts |
| Importing an existing metadata-format repo |
sf project convert mdapi (pattern 6) |
| Setting team conventions, review rules, branch model |
references/repo-conventions.md |
Related skills: sf-cli-operations (flags, config, API version resolution),
sf-deployment-strategies (deploy/validate/quick-deploy mechanics and test levels),
sf-scratch-orgs-sandboxes (org lifecycle, source tracking availability),
sf-packaging-release (package directories becoming 2GP/unlocked packages),
sf-security-model (profile vs permission-set policy).
Canonical layout
my-project/
├── sfdx-project.json project definition (commit)
├── .forceignore what the project commands exclude (commit)
├── .gitignore what git excludes (commit)
├── package.json LWC Jest, eslint, prettier dev deps (commit)
├── jest.config.js sfdx-lwc-jest config (commit)
├── config/
│ ├── project-scratch-def.json scratch org definition (commit)
│ └── dev-sandbox-def.json sandbox definition (commit)
├── manifest/
│ ├── package.xml deployment manifest (commit)
│ └── destructiveChangesPost.xml (commit when a release deletes metadata)
├── force-app/ default package directory
│ └── main/default/
│ ├── classes/ ApexClass: Foo.cls + Foo.cls-meta.xml
│ ├── triggers/ ApexTrigger: Foo.trigger + Foo.trigger-meta.xml
│ ├── lwc/<bundle>/ LightningComponentBundle (strict directory name)
│ ├── aura/<bundle>/ AuraDefinitionBundle (strict directory name)
│ ├── objects/<Object>/ decomposed: fields/, recordTypes/, listViews/, ...
│ ├── flows/ Flow: Foo.flow-meta.xml
│ ├── permissionsets/ PermissionSet: Foo.permissionset-meta.xml
│ ├── layouts/ Layout: Object-Layout Name.layout-meta.xml
│ ├── flexipages/ FlexiPage: Foo.flexipage-meta.xml
│ ├── labels/ CustomLabels: CustomLabels.labels-meta.xml
│ ├── staticresources/ StaticResource: Foo.resource-meta.xml + payload
│ └── settings/ Settings: Case.settings-meta.xml
├── .vibeforce/ vibe-force config + state + reports
├── .sf/ .sfdx/ .localdevserver/ CLI-generated; never edit, never commit
└── node_modules/ never commit
sfdx-project.json quick reference
| Property |
Required |
Purpose |
packageDirectories |
yes |
Array of { "path": "...", "default": true }. Relative paths only. Exactly one default; with a single entry it is implied |
sourceApiVersion |
recommended |
API version the metadata conforms to — becomes <version> in the generated manifest. vibe-force sets 67.0 |
namespace |
optional |
Namespace applied to scratch orgs created from this project; must be linked to the Dev Hub |
sfdcLoginUrl |
optional |
Login URL for sf org login *; default https://login.salesforce.com. Use the My Domain form for sandboxes |
oauthLocalPort |
optional |
OAuth callback port; default 1717. Must match the connected app callback URL |
packageAliases |
optional |
CLI-maintained map of readable names to 0Ho/04t package ids |
plugins |
optional |
Per-plugin settings you want version-controlled |
replacements |
optional |
Deploy-time string/regex substitution driven by env vars or files |
sourceBehaviorOptions |
optional (Beta) |
Extra decomposed types. Never hand-edit — run sf project convert source-behavior |
pushPackageDirectoriesSequentially |
deprecated |
Only affected the retired force:source:push; has no effect on sf project deploy start |
Full property tables, packaging fields, and replacements variants:
references/sfdx-project-schema.md.
Core patterns
1. Start the project, pin the API version
sf template generate project --name my-project --default-package-dir force-app \
--manifest --api-version 67.0
cd my-project
{
"packageDirectories": [{ "path": "force-app", "default": true }],
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "67.0"
}
Keep sourceApiVersion equal to the vibe-force apiVersion (config/vibe-force.defaults.json) and
to org-api-version; sf doctor runs a sourceApiVersion matches apiVersion check and warns when
they diverge. sourceApiVersion governs the shape of the metadata: deploying a file that contains
a field introduced in a later API version fails if sourceApiVersion is older, and retrieving with
an older value silently drops newer subelements.
2. Model the domain with multiple package directories
{
"packageDirectories": [
{ "path": "force-app", "default": true },
{ "path": "sales-ext" },
{ "path": "service-ext" },
{ "path": "unpackaged" }
],
"sourceApiVersion": "67.0"
}
Each directory repeats the standard main/default/... tree, so MyObject__c can live in
force-app while a new field on it lives in sales-ext/main/default/objects/MyObject__c/fields/.
Facts that matter:
- The split is client-side only. Deploying does not associate a directory with a package in the org.
sf project deploy start with no targeting flag deploys all package directories in one
transaction. Order is not guaranteed, so sequence explicitly when you need it:
sf project deploy start --source-dir force-app --target-org vf-dev
sf project deploy start --source-dir sales-ext --source-dir service-ext --target-org vf-dev
- The default directory is the retrieve target and the default output directory for conversions.
- These directories are the natural boundary for later unlocked/2GP packages (skill
sf-packaging-release), and the natural ownership boundary for the vibe-force wave-1 agents.
3. Know the source format rules that surprise people
| Rule |
Detail |
| Custom objects and custom object translations are always decomposed |
objects/<Object>/<Object>.object-meta.xml plus fields/, recordTypes/, listViews/, validationRules/, compactLayouts/, fieldSets/, businessProcesses/, indexes/, sharingReasons/, webLinks/ |
| Optional decomposition is opt-in and beta |
CustomLabels, PermissionSet, SharingRules, Workflow, ExternalServiceRegistration via sf project convert source-behavior --behavior <value> |
| Bundles need exact directory names |
lwc, aura, staticresources, experiences, documents, objects, objectTranslations, sites, emailservices, waveTemplates, lightningTypes, genAiFunctions, restrictionRules, bots, aiAuthoringBundles are strictDirectoryName types — a bundle in the wrong folder is invisible to the CLI |
Static resources live only in <pkg>/main/default/staticresources |
.zip/.jar archives are auto-expanded on retrieve; an archive that exists as a single file stays a single file. New resources use their MIME extension (.gif, .png); legacy .resource files keep that extension |
| Content types have two files |
ApexClass, ApexTrigger, ApexPage, ApexComponent, EmailTemplate, StaticResource ship a payload plus -meta.xml. Treat them as a pair in .forceignore and in reviews |
| Names with special characters are URL-encoded on disk |
Custom: Marketing Profile becomes Custom%3A Marketing Profile.profile-meta.xml, and .forceignore must use the encoded name |
| Folder-based types nest one level |
reports/<Folder>/<Report>.report-meta.xml, same for dashboards, documents, email |
4. Write .forceignore deliberately
.forceignore uses .gitignore syntax and is honoured by project deploy start,
project retrieve start, project convert source, and project delete source.
# Never deploy tooling and local artifacts
**/jsconfig.json
**/.eslintrc.json
**/__tests__/**
.sf/
.sfdx/
.localdevserver/
# Org-owned, human-managed metadata
**/profiles/**
**/*.settings-meta.xml
**/labels/CustomLabels.labels-meta.xml
# A specific component (MetadataWithContent needs both files, or a trailing *)
force-app/main/default/classes/LegacyBatch.cls*
# A whole bundle
**/lwc/legacyComponent
Precedence: when the commands decide whether to exclude a file they walk up from that file until
they find the first .forceignore, consult it, and stop. A .forceignore inside sales-ext/
therefore fully overrides the project-root one for files under sales-ext/. Put the primary file at
the project root; a per-package file is legitimate but must be documented.
Independently of .forceignore, the project commands always skip dot-files and dot-directories,
files ending in .dup, package2-descriptor.json, and package2-manifest.json.
Verify, never guess:
sf project list ignored
sf project list ignored --source-dir force-app/main/default/classes
sf project deploy preview --target-org vf-dev # shows Will Deploy / Will Delete / Conflicts / Ignored
vf-check static fails when a changed file is silently ignored by .forceignore but expected to be
part of the story, which is the common cause of "it works in my scratch org but not in UAT".
5. Operate source tracking honestly
| Org type |
Source tracking |
| Scratch org |
Always available, on by default |
| Developer / Developer Pro sandbox |
Available when the production org has source tracking enabled |
| Developer Edition, production, Partial Copy, Full sandbox |
Not supported |
sf project deploy preview --target-org vf-dev # local changes, deletions, conflicts, ignored
sf project retrieve preview --target-org vf-dev # remote changes
sf project deploy start --target-org vf-dev # deploy exactly the changed, tracked source
sf project retrieve start --target-org vf-dev # retrieve exactly the remote changes
sf org enable tracking --target-org vf-sandbox
sf org disable tracking --target-org vf-sandbox
sf project reset tracking --target-org vf-dev --revision 12345
sf project delete tracking --target-org vf-dev # forget all local tracking state
Conflict resolution: preview reports Conflicts [n]. Resolve the file, then overwrite one side
deliberately and narrowly:
# Keep the local version of one class, then continue normally.
sf project deploy start --metadata ApexClass:WidgetClass --ignore-conflicts --target-org vf-dev
# Or keep the org version of that class.
sf project retrieve start --metadata ApexClass:WidgetClass --ignore-conflicts --target-org vf-dev
sf project deploy start --target-org vf-dev
--ignore-conflicts silently discards the other side's work. Scope it to the component you decided
about; never add it to a whole-project command "to make the error go away". vibe-force hooks warn on
--ignore-conflicts in standard mode and block it against a production alias in strict mode.
Deleting metadata needs sf project delete source, which removes it from the org and the local
project; plain file deletion plus deploy start does not delete anything in the org unless the
component is tracked as deleted.
sf project delete source --metadata ApexClass:ObsoleteService --target-org vf-dev --check-only
sf project delete source --metadata ApexClass:ObsoleteService --target-org vf-dev --no-prompt
Performance: tracking adds SourceMember queries and polling. A "medium" project is 30+ components
or 50+ tests, "large" is 600+ components or 150+ tests; above that, deploy in smaller sets or tune
SF_SOURCE_TRACKING_BATCH_SIZE (see skill sf-cli-operations).
6. Convert between formats when you inherit a repo
# Metadata format (retrieve output, change-set export) -> source format
sf project convert mdapi --root-dir ./mdapi-out --output-dir force-app
# Source format -> metadata format, for a tool that only speaks Metadata API
sf project convert source --source-dir force-app --output-dir mdapi-package --package-name MyApp
For project convert mdapi, the applicable .forceignore is the one in the metadata retrieve
directory (next to package.xml), not the project root.
7. Keep .vibeforce/ and generated directories straight
| Path |
Commit? |
Content |
.vibeforce/config.json |
yes |
Project gates, org aliases, hook mode — reviewed like code |
.vibeforce/state/ownership.json |
no |
Wave-1 path ownership for the current story |
.vibeforce/state/deploy-jobs.json |
no |
Validation job ids for quick deploy |
.vibeforce/state/contract.md |
optional |
Cross-slice contract written in wave 0; commit it when it documents a decision worth keeping |
.vibeforce/reports/*.json |
no |
Check-runner output |
.sf/, .sfdx/ |
no |
CLI internals, auth, local config — never edit by hand |
.localdevserver/ |
no |
Local dev server cache (skill sf-local-development) |
node_modules/, coverage/, *.log |
no |
Build artifacts |
vf-init seeds a consumer project from this plugin's templates/ directory (project config seed,
formatter and lint configuration, .forceignore/.gitignore fragments) and creates
.vibeforce/; the exact inventory is whatever templates/ in the installed plugin contains.
Anti-patterns
| Anti-pattern |
Consequence |
Fix |
Two "default": true package directories |
The CLI cannot resolve the retrieve/convert target |
Exactly one default |
Absolute paths in packageDirectories |
Breaks on every other machine and in CI |
Relative paths ("force-app", "./force-app") |
Hand-editing sourceBehaviorOptions |
Project claims decomposition that the files do not have; deploys fail |
sf project convert source-behavior --behavior <value> (--dry-run first) |
| Deleting a metadata file and deploying |
Component stays in the org |
sf project delete source, or a destructiveChanges*.xml manifest |
--ignore-conflicts on a whole-project deploy |
Silently overwrites teammates' org changes |
Resolve per component, then deploy |
Committing .sf/ or .sfdx/ |
Leaks auth material and machine-local config |
Gitignore both; rotate anything already pushed |
| Profiles deployed from source alongside permission sets |
Profile XML is org-wide and merge-hostile; diffs are unreviewable |
Permission sets + permission set groups; ignore **/profiles/** (skill sf-security-model) |
One giant force-app for a multi-team org |
Every story touches the same tree; ownership and packaging are impossible |
Package directory per domain (pattern 2) |
sourceApiVersion left at whatever the project template shipped |
Newer subelements are dropped on retrieve and rejected on deploy |
Pin to the vibe-force apiVersion |
.forceignore entry naming only Foo.cls |
Foo.cls-meta.xml still deploys |
Foo.cls* or list both files |
Verification
# Project definition parses and resolves the way you think
node -e "const p=require('./sfdx-project.json');console.log(p.sourceApiVersion, p.packageDirectories)"
sf project list ignored
sf project deploy preview --target-org vf-dev
sf project retrieve preview --target-org vf-dev
# Manifest round-trip
sf project generate manifest --source-dir force-app --name manifest/package.xml
sf project deploy start --manifest manifest/package.xml --dry-run --target-org vf-dev
# vibe-force gates
node "$VF_ROOT/scripts/checks/vf-check.mjs" static # format + lint + analyzer on changed files
node "$VF_ROOT/scripts/checks/vf-check.mjs" local # static + jest
node "$VF_ROOT/scripts/checks/vf-check.mjs" deploy-validate --target-org vf-int
vf-check format and vf-check lint resolve files through packageDirectories, so a directory
missing from sfdx-project.json is also invisible to the gates — a strong reason to keep the file
accurate.
References
references/sfdx-project-schema.md — every property, packaging fields, replacements variants, worked examples.
references/metadata-directory-map.md — metadata type → directory → file suffix → notes, including decomposed children and strict-directory bundles.
references/forceignore-and-manifests.md — .forceignore patterns and precedence, package.xml, destructiveChangesPre/Post.xml, wildcard support.
references/xml-token-economy.md — measuring, reading and patching metadata XML without loading whole files; the xml-bulk-read guard and /vf-xml.
references/repo-conventions.md — branch-per-story, ownership, metadata review checklist, profile policy, monorepo tradeoffs, gitignore companion.
- Salesforce DX Project Structure and Source Format: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm
- Salesforce DX Project Configuration: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm
- Multiple Package Directories: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_mpd.htm
- How to Exclude Source When Syncing: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
- Track Changes Between Your Project and Org: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_tracking.htm
- Metadata API Developer Guide, Deploying and Retrieving Metadata: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm
1---2name: sf-project-structure3description: Structuring a Salesforce DX repository — the full `sfdx-project.json` schema (packageDirectories, default, sourceApiVersion, namespace, sfdcLoginUrl, plugins, packageAliases, replacements, sourceBehaviorOptions, oauthLocalPort, pushPackageDirectoriesSequentially), source-format directory layout for every metadata type (`objects/<Obj>/fields`, `classes`, `triggers`, `lwc`, `aura`, `flows`, `permissionsets`, `layouts`, `flexipages`, `staticresources`, `labels`, `settings`), decomposed metadata types, multiple package directories and modularisation, `.forceignore` semantics and precedence, `package.xml` and `destructiveChangesPre/Post.xml` manifests with `sf project generate manifest`, the source-tracking model (`sf project deploy|retrieve start|preview`, `delete source`, `reset tracking`, `--ignore-conflicts`), source/metadata format conversion, team repo conventions, and the `.vibeforce/` layout. Use this skill when creating or reorganising a DX project, deciding where a metadata file belongs, writing or audit4---56# Salesforce DX Project Structure78A directory is a Salesforce DX project because it contains `sfdx-project.json`. That file tells the9CLI which directories hold source (`packageDirectories`), what metadata shape to speak10(`sourceApiVersion`), and where to log in (`sfdcLoginUrl`).1112## When to use1314| Situation | Action |15| --- | --- |16| Creating a repo or adding a package directory | `sfdx-project.json` schema + multiple-package-directory rules below |17| "Where does this metadata file go?" | [`references/metadata-directory-map.md`](references/metadata-directory-map.md) |18| Source keeps getting deployed that should not be | `.forceignore` precedence, pattern 4, and `sf project list ignored` |19| Building a deployment or deletion manifest | `sf project generate manifest`, [`references/forceignore-and-manifests.md`](references/forceignore-and-manifests.md) |20| `sf project deploy start` reports conflicts | Pattern 5 — resolve, then deploy or retrieve with `--ignore-conflicts` |21| Importing an existing metadata-format repo | `sf project convert mdapi` (pattern 6) |22| Setting team conventions, review rules, branch model | [`references/repo-conventions.md`](references/repo-conventions.md) |2324Related skills: `sf-cli-operations` (flags, config, API version resolution),25`sf-deployment-strategies` (deploy/validate/quick-deploy mechanics and test levels),26`sf-scratch-orgs-sandboxes` (org lifecycle, source tracking availability),27`sf-packaging-release` (package directories becoming 2GP/unlocked packages),28`sf-security-model` (profile vs permission-set policy).2930## Canonical layout3132```text33my-project/34├── sfdx-project.json project definition (commit)35├── .forceignore what the project commands exclude (commit)36├── .gitignore what git excludes (commit)37├── package.json LWC Jest, eslint, prettier dev deps (commit)38├── jest.config.js sfdx-lwc-jest config (commit)39├── config/40│ ├── project-scratch-def.json scratch org definition (commit)41│ └── dev-sandbox-def.json sandbox definition (commit)42├── manifest/43│ ├── package.xml deployment manifest (commit)44│ └── destructiveChangesPost.xml (commit when a release deletes metadata)45├── force-app/ default package directory46│ └── main/default/47│ ├── classes/ ApexClass: Foo.cls + Foo.cls-meta.xml48│ ├── triggers/ ApexTrigger: Foo.trigger + Foo.trigger-meta.xml49│ ├── lwc/<bundle>/ LightningComponentBundle (strict directory name)50│ ├── aura/<bundle>/ AuraDefinitionBundle (strict directory name)51│ ├── objects/<Object>/ decomposed: fields/, recordTypes/, listViews/, ...52│ ├── flows/ Flow: Foo.flow-meta.xml53│ ├── permissionsets/ PermissionSet: Foo.permissionset-meta.xml54│ ├── layouts/ Layout: Object-Layout Name.layout-meta.xml55│ ├── flexipages/ FlexiPage: Foo.flexipage-meta.xml56│ ├── labels/ CustomLabels: CustomLabels.labels-meta.xml57│ ├── staticresources/ StaticResource: Foo.resource-meta.xml + payload58│ └── settings/ Settings: Case.settings-meta.xml59├── .vibeforce/ vibe-force config + state + reports60├── .sf/ .sfdx/ .localdevserver/ CLI-generated; never edit, never commit61└── node_modules/ never commit62```6364## `sfdx-project.json` quick reference6566| Property | Required | Purpose |67| --- | --- | --- |68| `packageDirectories` | yes | Array of `{ "path": "...", "default": true }`. Relative paths only. Exactly one `default`; with a single entry it is implied |69| `sourceApiVersion` | recommended | API version the *metadata* conforms to — becomes `<version>` in the generated manifest. vibe-force sets `67.0` |70| `namespace` | optional | Namespace applied to scratch orgs created from this project; must be linked to the Dev Hub |71| `sfdcLoginUrl` | optional | Login URL for `sf org login *`; default `https://login.salesforce.com`. Use the My Domain form for sandboxes |72| `oauthLocalPort` | optional | OAuth callback port; default 1717. Must match the connected app callback URL |73| `packageAliases` | optional | CLI-maintained map of readable names to `0Ho`/`04t` package ids |74| `plugins` | optional | Per-plugin settings you want version-controlled |75| `replacements` | optional | Deploy-time string/regex substitution driven by env vars or files |76| `sourceBehaviorOptions` | optional (Beta) | Extra decomposed types. Never hand-edit — run `sf project convert source-behavior` |77| `pushPackageDirectoriesSequentially` | deprecated | Only affected the retired `force:source:push`; has no effect on `sf project deploy start` |7879Full property tables, packaging fields, and `replacements` variants:80[`references/sfdx-project-schema.md`](references/sfdx-project-schema.md).8182## Core patterns8384### 1. Start the project, pin the API version8586```bash87sf template generate project --name my-project --default-package-dir force-app \88 --manifest --api-version 67.089cd my-project90```9192```json93{94 "packageDirectories": [{ "path": "force-app", "default": true }],95 "namespace": "",96 "sfdcLoginUrl": "https://login.salesforce.com",97 "sourceApiVersion": "67.0"98}99```100101Keep `sourceApiVersion` equal to the vibe-force `apiVersion` (`config/vibe-force.defaults.json`) and102to `org-api-version`; `sf doctor` runs a `sourceApiVersion matches apiVersion` check and warns when103they diverge. `sourceApiVersion` governs the *shape of the metadata*: deploying a file that contains104a field introduced in a later API version fails if `sourceApiVersion` is older, and retrieving with105an older value silently drops newer subelements.106107### 2. Model the domain with multiple package directories108109```json110{111 "packageDirectories": [112 { "path": "force-app", "default": true },113 { "path": "sales-ext" },114 { "path": "service-ext" },115 { "path": "unpackaged" }116 ],117 "sourceApiVersion": "67.0"118}119```120121Each directory repeats the standard `main/default/...` tree, so `MyObject__c` can live in122`force-app` while a new field on it lives in `sales-ext/main/default/objects/MyObject__c/fields/`.123Facts that matter:124125- The split is client-side only. Deploying does not associate a directory with a package in the org.126- `sf project deploy start` with no targeting flag deploys all package directories in one127 transaction. Order is not guaranteed, so sequence explicitly when you need it:128129```bash130sf project deploy start --source-dir force-app --target-org vf-dev131sf project deploy start --source-dir sales-ext --source-dir service-ext --target-org vf-dev132```133134- The default directory is the retrieve target and the default output directory for conversions.135- These directories are the natural boundary for later unlocked/2GP packages (skill136 `sf-packaging-release`), and the natural ownership boundary for the vibe-force wave-1 agents.137138### 3. Know the source format rules that surprise people139140| Rule | Detail |141| --- | --- |142| Custom objects and custom object translations are always decomposed | `objects/<Object>/<Object>.object-meta.xml` plus `fields/`, `recordTypes/`, `listViews/`, `validationRules/`, `compactLayouts/`, `fieldSets/`, `businessProcesses/`, `indexes/`, `sharingReasons/`, `webLinks/` |143| Optional decomposition is opt-in and beta | `CustomLabels`, `PermissionSet`, `SharingRules`, `Workflow`, `ExternalServiceRegistration` via `sf project convert source-behavior --behavior <value>` |144| Bundles need exact directory names | `lwc`, `aura`, `staticresources`, `experiences`, `documents`, `objects`, `objectTranslations`, `sites`, `emailservices`, `waveTemplates`, `lightningTypes`, `genAiFunctions`, `restrictionRules`, `bots`, `aiAuthoringBundles` are `strictDirectoryName` types — a bundle in the wrong folder is invisible to the CLI |145| Static resources live only in `<pkg>/main/default/staticresources` | `.zip`/`.jar` archives are auto-expanded on retrieve; an archive that exists as a single file stays a single file. New resources use their MIME extension (`.gif`, `.png`); legacy `.resource` files keep that extension |146| Content types have two files | `ApexClass`, `ApexTrigger`, `ApexPage`, `ApexComponent`, `EmailTemplate`, `StaticResource` ship a payload plus `-meta.xml`. Treat them as a pair in `.forceignore` and in reviews |147| Names with special characters are URL-encoded on disk | `Custom: Marketing Profile` becomes `Custom%3A Marketing Profile.profile-meta.xml`, and `.forceignore` must use the encoded name |148| Folder-based types nest one level | `reports/<Folder>/<Report>.report-meta.xml`, same for `dashboards`, `documents`, `email` |149150### 4. Write `.forceignore` deliberately151152`.forceignore` uses `.gitignore` syntax and is honoured by `project deploy start`,153`project retrieve start`, `project convert source`, and `project delete source`.154155```gitignore156# Never deploy tooling and local artifacts157**/jsconfig.json158**/.eslintrc.json159**/__tests__/**160.sf/161.sfdx/162.localdevserver/163164# Org-owned, human-managed metadata165**/profiles/**166**/*.settings-meta.xml167**/labels/CustomLabels.labels-meta.xml168169# A specific component (MetadataWithContent needs both files, or a trailing *)170force-app/main/default/classes/LegacyBatch.cls*171172# A whole bundle173**/lwc/legacyComponent174```175176Precedence: when the commands decide whether to exclude a file they walk **up** from that file until177they find the first `.forceignore`, consult it, and stop. A `.forceignore` inside `sales-ext/`178therefore fully overrides the project-root one for files under `sales-ext/`. Put the primary file at179the project root; a per-package file is legitimate but must be documented.180181Independently of `.forceignore`, the project commands always skip dot-files and dot-directories,182files ending in `.dup`, `package2-descriptor.json`, and `package2-manifest.json`.183184Verify, never guess:185186```bash187sf project list ignored188sf project list ignored --source-dir force-app/main/default/classes189sf project deploy preview --target-org vf-dev # shows Will Deploy / Will Delete / Conflicts / Ignored190```191192`vf-check static` fails when a changed file is silently ignored by `.forceignore` but expected to be193part of the story, which is the common cause of "it works in my scratch org but not in UAT".194195### 5. Operate source tracking honestly196197| Org type | Source tracking |198| --- | --- |199| Scratch org | Always available, on by default |200| Developer / Developer Pro sandbox | Available when the production org has source tracking enabled |201| Developer Edition, production, Partial Copy, Full sandbox | Not supported |202203```bash204sf project deploy preview --target-org vf-dev # local changes, deletions, conflicts, ignored205sf project retrieve preview --target-org vf-dev # remote changes206207sf project deploy start --target-org vf-dev # deploy exactly the changed, tracked source208sf project retrieve start --target-org vf-dev # retrieve exactly the remote changes209210sf org enable tracking --target-org vf-sandbox211sf org disable tracking --target-org vf-sandbox212sf project reset tracking --target-org vf-dev --revision 12345213sf project delete tracking --target-org vf-dev # forget all local tracking state214```215216Conflict resolution: `preview` reports `Conflicts [n]`. Resolve the file, then overwrite one side217deliberately and narrowly:218219```bash220# Keep the local version of one class, then continue normally.221sf project deploy start --metadata ApexClass:WidgetClass --ignore-conflicts --target-org vf-dev222# Or keep the org version of that class.223sf project retrieve start --metadata ApexClass:WidgetClass --ignore-conflicts --target-org vf-dev224sf project deploy start --target-org vf-dev225```226227`--ignore-conflicts` silently discards the other side's work. Scope it to the component you decided228about; never add it to a whole-project command "to make the error go away". vibe-force hooks warn on229`--ignore-conflicts` in `standard` mode and block it against a production alias in `strict` mode.230231Deleting metadata needs `sf project delete source`, which removes it from the org **and** the local232project; plain file deletion plus `deploy start` does not delete anything in the org unless the233component is tracked as deleted.234235```bash236sf project delete source --metadata ApexClass:ObsoleteService --target-org vf-dev --check-only237sf project delete source --metadata ApexClass:ObsoleteService --target-org vf-dev --no-prompt238```239240Performance: tracking adds `SourceMember` queries and polling. A "medium" project is 30+ components241or 50+ tests, "large" is 600+ components or 150+ tests; above that, deploy in smaller sets or tune242`SF_SOURCE_TRACKING_BATCH_SIZE` (see skill `sf-cli-operations`).243244### 6. Convert between formats when you inherit a repo245246```bash247# Metadata format (retrieve output, change-set export) -> source format248sf project convert mdapi --root-dir ./mdapi-out --output-dir force-app249250# Source format -> metadata format, for a tool that only speaks Metadata API251sf project convert source --source-dir force-app --output-dir mdapi-package --package-name MyApp252```253254For `project convert mdapi`, the applicable `.forceignore` is the one in the metadata retrieve255directory (next to `package.xml`), not the project root.256257### 7. Keep `.vibeforce/` and generated directories straight258259| Path | Commit? | Content |260| --- | --- | --- |261| `.vibeforce/config.json` | yes | Project gates, org aliases, hook mode — reviewed like code |262| `.vibeforce/state/ownership.json` | no | Wave-1 path ownership for the current story |263| `.vibeforce/state/deploy-jobs.json` | no | Validation job ids for quick deploy |264| `.vibeforce/state/contract.md` | optional | Cross-slice contract written in wave 0; commit it when it documents a decision worth keeping |265| `.vibeforce/reports/*.json` | no | Check-runner output |266| `.sf/`, `.sfdx/` | no | CLI internals, auth, local config — never edit by hand |267| `.localdevserver/` | no | Local dev server cache (skill `sf-local-development`) |268| `node_modules/`, `coverage/`, `*.log` | no | Build artifacts |269270`vf-init` seeds a consumer project from this plugin's `templates/` directory (project config seed,271formatter and lint configuration, `.forceignore`/`.gitignore` fragments) and creates272`.vibeforce/`; the exact inventory is whatever `templates/` in the installed plugin contains.273274## Anti-patterns275276| Anti-pattern | Consequence | Fix |277| --- | --- | --- |278| Two `"default": true` package directories | The CLI cannot resolve the retrieve/convert target | Exactly one default |279| Absolute paths in `packageDirectories` | Breaks on every other machine and in CI | Relative paths (`"force-app"`, `"./force-app"`) |280| Hand-editing `sourceBehaviorOptions` | Project claims decomposition that the files do not have; deploys fail | `sf project convert source-behavior --behavior <value>` (`--dry-run` first) |281| Deleting a metadata file and deploying | Component stays in the org | `sf project delete source`, or a `destructiveChanges*.xml` manifest |282| `--ignore-conflicts` on a whole-project deploy | Silently overwrites teammates' org changes | Resolve per component, then deploy |283| Committing `.sf/` or `.sfdx/` | Leaks auth material and machine-local config | Gitignore both; rotate anything already pushed |284| Profiles deployed from source alongside permission sets | Profile XML is org-wide and merge-hostile; diffs are unreviewable | Permission sets + permission set groups; ignore `**/profiles/**` (skill `sf-security-model`) |285| One giant `force-app` for a multi-team org | Every story touches the same tree; ownership and packaging are impossible | Package directory per domain (pattern 2) |286| `sourceApiVersion` left at whatever the project template shipped | Newer subelements are dropped on retrieve and rejected on deploy | Pin to the vibe-force `apiVersion` |287| `.forceignore` entry naming only `Foo.cls` | `Foo.cls-meta.xml` still deploys | `Foo.cls*` or list both files |288289## Verification290291```bash292# Project definition parses and resolves the way you think293node -e "const p=require('./sfdx-project.json');console.log(p.sourceApiVersion, p.packageDirectories)"294sf project list ignored295sf project deploy preview --target-org vf-dev296sf project retrieve preview --target-org vf-dev297298# Manifest round-trip299sf project generate manifest --source-dir force-app --name manifest/package.xml300sf project deploy start --manifest manifest/package.xml --dry-run --target-org vf-dev301302# vibe-force gates303node "$VF_ROOT/scripts/checks/vf-check.mjs" static # format + lint + analyzer on changed files304node "$VF_ROOT/scripts/checks/vf-check.mjs" local # static + jest305node "$VF_ROOT/scripts/checks/vf-check.mjs" deploy-validate --target-org vf-int306```307308`vf-check format` and `vf-check lint` resolve files through `packageDirectories`, so a directory309missing from `sfdx-project.json` is also invisible to the gates — a strong reason to keep the file310accurate.311312## References313314- [`references/sfdx-project-schema.md`](references/sfdx-project-schema.md) — every property, packaging fields, `replacements` variants, worked examples.315- [`references/metadata-directory-map.md`](references/metadata-directory-map.md) — metadata type → directory → file suffix → notes, including decomposed children and strict-directory bundles.316- [`references/forceignore-and-manifests.md`](references/forceignore-and-manifests.md) — `.forceignore` patterns and precedence, `package.xml`, `destructiveChangesPre/Post.xml`, wildcard support.317- [`references/xml-token-economy.md`](references/xml-token-economy.md) — measuring, reading and patching metadata XML without loading whole files; the `xml-bulk-read` guard and `/vf-xml`.318- [`references/repo-conventions.md`](references/repo-conventions.md) — branch-per-story, ownership, metadata review checklist, profile policy, monorepo tradeoffs, gitignore companion.319- Salesforce DX Project Structure and Source Format: <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm>320- Salesforce DX Project Configuration: <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm>321- Multiple Package Directories: <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_mpd.htm>322- How to Exclude Source When Syncing: <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm>323- Track Changes Between Your Project and Org: <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_tracking.htm>324- Metadata API Developer Guide, Deploying and Retrieving Metadata: <https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm>