SPFx deployment is one file and one upload
An SPFx solution ships as one .sppkg uploaded to the app catalog, live tenant-wide in a minute. What is in the package, and the three places it still bites.
Most enterprise deployment stories are bad. There is a server, and the server has state, and the state is slightly different from the last server, and somewhere a service needs restarting during a window agreed three weeks ago.
SharePoint Framework does not work like that. A finished SPFx solution is one file. You upload it to a document library. It is live across the tenant in about a minute, with no downtime and nothing to restart. That is the entire deployment.
This is worth writing down because SharePoint’s own history works against it. Anyone who deployed farm solutions to on-premises SharePoint remembers the GAC, the timer jobs, the stsadm incantations, the application pool recycles that took the intranet down mid-afternoon. The instinct to expect pain is well earned. It is also, for SPFx, about a decade out of date.
What is actually in the package
The build produces a .sppkg file. Despite the extension it is an ordinary zip archive containing a solution manifest and — this is the part that matters — the bundled JavaScript and CSS themselves.
That second part is easy to skip past. It means there is no separate asset hosting to arrange. Older SPFx guidance had you publish your bundles to an Office 365 CDN or an Azure storage account and point the manifest at the URL, which turned one deployment into two, with a cache invalidation problem in between. With includeClientSideAssets set to true in config/package-solution.json — the default for a long time now — the assets ride inside the package and get served from the app catalog itself.
One artifact. No CDN to configure, no storage account to pay for, no second thing to keep in sync with the first.
Building it
Note that the toolchain changed recently, and a lot of the tutorials still online are describing the old one.
Through SPFx 1.21.1, builds ran on gulp, and the commands everyone had memorised were gulp bundle --ship and gulp package-solution --ship. Starting with 1.22, new projects use Heft as the build orchestrator instead. Webpack still does the bundling underneath; the layer above it is different.
For a current project — 1.23.2 is the GA release as of June 2026 — the production build is two npm scripts:
npm run build -- --production
npm run package-solution -- --production
Go through the npm scripts rather than calling Heft directly. The scripts are where lifecycle hooks and any pipeline customisations are wired in, and calling the underlying binary skips them silently.
The --production flag is the one to get right. Without it you get a development build: unminified, source-mapped, and in some configurations still pointing at your local dev server. It will upload happily and it will fail for everyone but you. If a deployment works on your machine and nowhere else, check this first.
The output lands in sharepoint/solution/ as a single .sppkg.
Node 22 LTS is the required runtime for 1.22 and later. SPFx has always been fussy about Node versions in a way that punishes casual upgrades, so pin it in CI and use a version manager locally.
Uploading it
Open the tenant app catalog, drag the .sppkg into the library, and SharePoint shows a trust dialog summarising what the solution does and where its code comes from.
If the solution has skipFeatureDeployment: true in package-solution.json, that dialog includes a checkbox — Enable this app and add it to all sites — and it is ticked by default. Leave it ticked and the web part appears in the web part picker on every site in the tenant immediately. No per-site installation, no site owners needing to add anything, no rollout ticket.
That is the whole deployment. Drag, confirm, done.
For a narrower blast radius, site collection app catalogs work the same way at a single-site scope, which is the usual pattern for piloting something with one department before it goes tenant-wide.
Updating is the same three steps
Bump solution.version in config/package-solution.json — it is a four-part version like 1.0.1.0 — rebuild, and upload the new package over the old one. SharePoint recognises it as an update and offers to deploy it.
Users get the new version on their next page load. There is no reinstall, no prompt, nothing for anyone to accept, and no window to schedule. The gap between “I have a fix” and “every user in the organization has the fix” is the length of the upload.
Rollback is correspondingly cheap, and for a reason that is more accident than design: the app catalog is an ordinary document library with versioning on, so the previous .sppkg is generally still sitting in the version history of the file you just overwrote. Restore the earlier version, redeploy, and you are back. Keeping your own copies of shipped packages as build artifacts is still the more disciplined habit, but it is reassuring that the platform hangs onto them regardless.
Three places it still bites
The story above is genuinely as smooth as it sounds. These are the parts that are not, and they are worth knowing before they surprise you.
API permissions are approved somewhere else, by someone else. If your solution calls Microsoft Graph, you declare what it needs via webApiPermissionRequests in package-solution.json. Uploading the package raises the request — it does not grant it. A tenant administrator has to approve it on the API access page in the SharePoint admin center, or via Approve-SPOTenantServicePrincipalPermissionRequest. Until that happens the solution deploys cleanly and then fails at runtime with permission errors, which reads like a code bug and is not one. If you are handing a package to a client’s admin team, say this explicitly in the handover notes.
Tenant-wide deployment and the feature framework are mutually exclusive. Setting skipFeatureDeployment: true is what buys you the “available everywhere immediately” behaviour, and the cost is that feature framework definitions in the solution are ignored. If your solution needs to provision a list, a content type, or a field when it lands on a site, you cannot also have the frictionless tenant-wide install. Pick one. Solutions that need provisioning generally do it at first run through code instead.
Central deployment is invisible at the site level. A centrally deployed solution does not appear under Add an app on individual sites. This is correct behaviour and it consistently confuses site owners who go looking for confirmation that the thing was installed. It was; that is just not where it shows up.
Automating it
None of this needs a human dragging files once it settles. The upload and deploy steps have supported command-line equivalents for years, so the whole path fits in a pipeline:
# build
npm ci
npm run build -- --production
npm run package-solution -- --production
# ship — CLI for Microsoft 365
m365 spo app add --filePath ./sharepoint/solution/my-solution.sppkg --overwrite
m365 spo app deploy --name my-solution.sppkg --skipFeatureDeployment
PnP PowerShell covers the same ground with Add-PnPApp and Publish-PnPApp if that fits your existing tooling better. Either way, a merge to main can put a new version in front of every user in the tenant without anyone opening a browser.
Why this is worth reaching for
The practical consequence is that the cost of shipping a small improvement to an intranet is close to zero. When deployment is a scheduled event with a rollback plan and a change ticket, small improvements do not get made — the overhead exceeds the value, so they queue up behind something big enough to justify the ceremony, and mostly never arrive.
When deployment is a file upload, a web part can be corrected the afternoon someone mentions the wording is confusing. That changes what kind of work is worth doing at all, which is a bigger deal than any individual feature.
It is a good deployment model. The main thing standing between most teams and it is an assumption, formed somewhere around 2012, that SharePoint development is painful.
Sources: SPFx roadmap update — July 2026, tenant-scoped deployment, Heft toolchain migration, connecting to Entra-secured APIs.