The three ways: buy, adapt, build yourself
The first way is the Shopware Store, and it is the right one when an extension essentially matches your requirement. For payment methods, shipping providers, review systems, invoice layouts or marketplace integrations there are well-maintained standard solutions, and building your own alongside them would be a waste. Two things belong in the calculation. First: since 28 December 2023 the rental licence is the only way to acquire an extension licence in the Shopware Store. When the rental period ends, the right to use the extension ends — that is a running item in your operating budget, not a one-off investment. Second: you take on the vendor's roadmap. Whether there is a compatible version when Shopware 6.8 is released is decided by the vendor, not by you. If a store extension covers your requirement, we say so, even though we then develop nothing.
The second way is adapting an existing extension, and it is regularly overlooked. The source code is readable: PHP and Twig in any case, and the quality guidelines for the Shopware Store require the uncompiled version of the JavaScript to be shipped in a separate directory. Adapting still does not mean editing inside somebody else's plugin — everything would be gone again with the next vendor update. We build a second, small plugin alongside it: it decorates the services of the third-party extension, hooks into its events, extends its Twig templates via sw_extends and, in the admin, overrides only the components it has to change. Both extensions remain separately updatable. The limit of this approach is sharp: if the third-party extension offers no extension points, hides its logic in final classes or private methods and sets no useful blocks in its templates, the approach ends there. We see this in advance, not in the middle of the project.
The third way is your own development. It is almost always the most expensive to buy and rarely the wrong choice over the lifetime of the system. You get exactly the function your process needs, no configuration screen for thirty cases you do not have, and no third-party logic in the request path of the checkout. No right of use that ends with a cancellation. In return you carry the maintenance yourself: every major step in Shopware costs adaptation, and that adaptation is then on your invoice. We compare both ways against each other if you ask us to, and the figure that counts is not the quoted price but the total over the years you intend to run the system. And some requirements are not plugin requirements at all: a plugin that automates an unclear rule makes the lack of clarity faster, not better.
Plugin or app: the difference that remains
A plugin is PHP code that runs inside the Shopware process itself. Technically it is a Composer package with a plugin base class and a lifecycle of install, activate, update, deactivate and uninstall. It has access to everything Shopware and Symfony provide: event subscribers, service decoration through the container, its own EntityDefinition and extensions of existing entities, its own storefront and admin routes, Twig overrides, its own admin components, scheduled tasks and message handlers. The price for this is dependency: a plugin can only be run in a self-hosted installation, not in Shopware Cloud. And it sits in the request path. Whatever time it uses there, the shop uses on every request.
An app brings no executable code into the shop, only a manifest.xml. The logic runs on its own app server, which talks to the shop through the Admin API, the Store API and webhooks — in a language of your choice. Apps run in Shopware Cloud and in self-hosted installations. For logic that has to happen inside the shop process there are App Scripts: Twig files that, since Shopware 6.4.8.0, are executed in a sandbox at defined hooks and see only the services that the respective hook provides. An app extends the administration through the Meteor Admin SDK; the interface comes from an iframe and communicates via postMessage. Your own data structures go through a Resources/entities.xml; the entity names usually carry the prefix ce_, which is possible and recommended since Shopware 6.4.15.0 because the full names fail at the length limits of the database. After that they appear as a regular entity in the Admin API and in the administration.
The decision is unspectacular. An app, if Shopware Cloud is involved, if the logic lives outside anyway — a price calculation in the ERP, an availability service, a middle layer to a third-party system — or if you want to deliver the extension to several shops. A plugin, if the work reaches deep into the data model, the storefront or the checkout, if latency matters, or if the function has to run without a network connection to the outside. Both disadvantages should be stated. The app server is a second system that you have to run, monitor and keep up to date — if it stops, the function stops. The plugin, on the other hand, lives in the shop's deployment and needs your attention at every major release. There is no option here without follow-up costs, only two different ones.
Developing to the standard so the next update is not blocked
Shopware promises extension developers backward compatibility, and this promise is pleasantly concrete. Public and protected methods, properties and constants count as public API, and so do interfaces and abstract classes, the constructors of DTOs such as CalculatedPrice or QuantityPriceDefinition, Twig templates and blocks of the storefront, the JavaScript plugins and services, the props, slots and events of the admin components, and CSS selectors with structural meaning. Breaking changes are only allowed in major releases; before that, the affected code is marked with an annotation such as @deprecated tag:v6.8.0, which names the target release and the replacement. Anyone who reads these annotations knows months before the major release what has to be done. There are two exceptions you have to know. First: the constructor of a service that is instantiated by the container is explicitly not public API and may change at any time — anyone who creates such a service themselves with new instead of decorating it builds in the blockage. Second: private methods, properties and constants are not covered, and neither is anything marked @internal or @experimental. A class marked as final, by contrast, is part of the public API — it may be used, only not inherited from. The plugins that block updates later sit at these edges.
In practice this comes down to a short list. Do not touch a core file — the quality guidelines for store extensions forbid changes to core files and core database tables in any case. Event subscribers instead of copied logic. Service decoration instead of replacing classes. Entity extension instead of ALTER TABLE. Twig extension with sw_extends and targeted block overrides instead of a copied template that quietly falls apart at the next storefront rebuild. In the admin, the intended override API instead of replacing files. Plus the things that show up later in operation: an uninstall routine that takes its data with it, snippets with a fallback language, proper logging instead of output into nowhere, and migrations that run a second time without doing damage.
Checking is cheaper than hoping, and Shopware supplies the tools itself. shopware-cli extension validate checks metadata, packaging, directory structure, snippets and PHP syntax at the basic level; with --full it adds PHPStan, ESLint, Stylelint and separate checks for admin and storefront Twig, each of which can be switched on individually via --only and --exclude. With --check-against lowest or highest, the check runs against the edges of your version constraint, and that is exactly where plugins break. extension format and extension fix clean up what can be cleaned up automatically; underneath, PHP CS Fixer, Prettier, Rector for PHP and ESLint for JavaScript do the work. The former Extension Verifier, a separate Docker image with the commands check, format and fix, was merged into shopware-cli in May 2025 — its repository is archived, and its checks sit in the commands above. So there are not two tools, only one. With us it runs in the CI, not on the laptop of one individual. But a linter does not find a mistake in reasoning. A plugin that follows every rule and solves the wrong problem is still wrong.
The break with 6.7: two lines, two releases
Shopware 6.7 is the biggest technical rebuild since Shopware 6 started, and it hits the administration hardest. Vue 3 instead of Vue 2 — the compatibility layer is removed, not deprecated. Vite instead of Webpack, so a webpack.config.js becomes a vite.config.js and the delivered build artefacts look different. Pinia instead of Vuex: the core stores are located under Shopware.Store instead of Shopware.State, and the old Vuex helpers only continue to exist under new names — mapState is now mapVuexState, mapMutations is mapVuexMutations, mapGetters is mapVuexGetters, mapActions is mapVuexActions. You can still create your own Vuex stores; it is not recommended. On top of that comes the move to the Meteor Component Library, in which sw-button becomes mt-button and the v-model bindings change. For you this means one thing: a 6.7 release of an extension is a separate package with its own build, not an adjusted version number.
The remaining edges sit in the PHP part and in the storefront. The core classes have been given native types on their properties throughout; every class that inherits from them has to follow with the types. Payment processing runs through a single AbstractPaymentHandler class that replaces five interfaces: AsyncPaymentHandlerInterface, SyncPaymentHandlerInterface, PreparedPaymentHandlerInterface, RefundPaymentHandlerInterface and RecurringPaymentHandlerInterface; a handler now only receives orderTransactionId, the request data as far as it is present, and the context. Payment and shipping methods need a technicalName, mandatory in the API and in the database — the technical_name column becomes non-nullable in payment_method and shipping_method. Custom field and fieldset names may no longer contain hyphens or dots; they have to be valid Twig variable names. Header and footer load via ESI through their own routes /header and /footer. Store API route caching has been dropped, and with it the Cached*Route classes. And for plugins the route via a Resources/config/entities.xml is gone — custom entities via XML have been removed for plugins; what belongs there now is your own EntityDefinition or attribute-based entities.
Anyone maintaining an extension for 6.6 and 6.7 at the same time maintains two lines: two branches, two CI runs, two test matrices and, in the store, two version states under one entry. The effort for this is closer to a major step than to a point release, and that is not an argument against 6.7 but a planning figure. For existing installations it has been much more relaxed since the beginning of 2026: the large store plugins have compatible versions, unlike in the first months after the release. Anyone who runs only their own shop does not need the second line anyway — then we port once to 6.7 and close 6.6. The update project itself, meaning the order of steps, the staging run and the rollback plan, has its own page; here we are talking about the code that has to move with it.
Orphaned plugins and taking over third-party code
When a vendor no longer delivers there are three ways forward, and the first look does not go into the code but into the licence. With a rental licence from the Shopware Store, the right of use ends with the rental period; whether you may then fork the code, develop it further or run it at all is first a legal question and only then a technical one. That is answered by your legal advisers, not by your agency. If the extension is available as open source on Packagist or GitHub, a fork is the shortest way: your own repository, your own constraint, your own release responsibility. The second way is replacement with a maintained alternative, even if configuration and data have to be moved in the process. The third is new development — and it has an underestimated advantage: the specification already exists, and it has been running in your shop for years.
Before we agree to a takeover we look at the same set of questions, every time. What is in the composer.json, in particular in the constraint against shopware/core and in the dependencies? Is there a regular plugin base class and a clean lifecycle, or does the installation happen in a migration? What does the uninstall leave behind? How many services are decorated, and does one of these decorations reach into internals that are marked @internal or count as private? Are storefront templates extended via sw_extends or copied? Do the overridden admin components still use Vue 2 patterns or Shopware.State? Do tests exist, and do they run? Plus a run of shopware-cli extension validate --full against the edges of the constraint and, where it makes sense, extension format and extension fix. This is work of hours to days, not minutes, and it is a defined first deliverable with its own price.
At the end there is a findings report: porting effort by area of work, a risk list with priorities and, next to it, the alternative of new development with its own figure. Both figures stand side by side, even if porting is the smaller job. And we decline if a plugin is built in such a way that we cannot vouch for its behaviour after the next update. An extension that reaches into undocumented internals in five places is not a porting case but a new development with an existing specification — and then it is more honest to call it that than to offer a port that has to be paid for a second time at the next major release. A commitment without a look at the code would be guesswork, and guessing is not a method with somebody else's code.