Shopware 6 provides two powerful ways to extend and customize your shop: Apps and Plugins. Both offer ways to interact with the platform, but they serve very different purposes. In this post, we’ll explore the differences through a simple (and slightly sneaky) example involving the GMV (Gross Merchandise Volume) of a shop.
What We’re Building
We’ll create a Shopware App that queries the total order value of the shop (GMV) via the Shopware Admin API.
Then, we’ll implement a Plugin that intercepts those order values and manipulates them – effectively feeding fake data back to the app. This is not a practical use case, but it's perfect to show how these two extension types operate.
Shopware Apps: External Logic with API Access
Apps in Shopware 6 are external applications that run on a separate server (not inside Shopware) and communicate with the shop via a secure API integration.
When an app is installed, it registers an integration with specific access scopes (permissions), which the shop admin has to approve. It’s important to note: the app only gets access to the data that the API makes available, and only if the proper permissions were granted.
Here’s an example manifest.xml
for our app:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Manifest/Schema/manifest-2.0.xsd">
FakeGmvReporting
Fake GMV Reporting
lang="de-DE">Fake GMV Reporting
Test app for reading orders to calculate GMV.
Friendly Hacker
1.0.0
MIT
http://fake-gmv-app:3000/register
supersecret
name="appActivated" url="http://fake-gmv-app:3000/activated" event="app.activated"/>
order
order_line_item
currency
system_config
✅ Shopware generates an integration with the exact set of API scopes you declare in the manifest.
Apps cannot access more than what’s explicitly listed – unless, of course, the scopes are overly broad.
👉 As a shop owner, always review which data tables are requested – system_config
is commonly used to extract sensitive config and credentials!
🛡 OAuth Flow: /register, /confirm, /activated
The app follows the standard Shopware app lifecycle:
/register
Shopware calls this during installation. The app responds with a cryptographic proof.
app.get('/register', (req, res) => {
const shopUrl = req.query['shop-url'];
const shopId = req.query['shop-id'];
const rawData = `${shopId}${shopUrl}${APP_NAME}`;
const proof = crypto
.createHmac('sha256', APP_SECRET)
.update(rawData)
.digest('hex');
res.json({
proof: proof,
secret: APP_SECRET,
confirmation_url: 'http://fake-gmv-app:3000/confirm'
});
});
/confirm
Shopware sends client credentials so the app can request access tokens.
app.post('/confirm', express.text({ type: 'application/json' }), (req, res) => {
const body = req.body;
const tokenData = {
shopId: body.shopId,
clientId: body.apiKey,
clientSecret: body.secretKey,
shopUrl: 'http://shopware'
};
fs.writeFileSync('./shop-token.json', JSON.stringify(tokenData, null, 2));
res.sendStatus(204);
});
⚠️ In this example, we store the API credentials in a local file – don’t do this in production!
This is just for demonstration. In a real app, use a secure key vault and avoid persisting credentials as plain text.
/activated
app.post('/activated', (req, res) => {
console.log('✅ App was activated');
res.sendStatus(200);
});
ℹ️ Note: Our Shopware instance is assumed to be running at http://shopware
, and our app lives at http://fake-gmv-app
.
📊 API Endpoint: /gmv
This is the core logic of our app – fetch orders and calculate GMV.
app.get('/gmv', async (req, res) => {
const tokenData = JSON.parse(fs.readFileSync('./shop-token.json', 'utf-8'));
const token = await fetchAccessToken(tokenData.clientId, tokenData.clientSecret, tokenData.shopUrl);
const response = await axios.get(`${tokenData.shopUrl}/api/order`, {
headers: { Authorization: `Bearer ${token}` }
});
const orders = response.data.data;
const gmv = orders.reduce((sum, order) => sum + order.amountTotal, 0);
res.json({
orders: orders.length,
gmv: gmv.toFixed(2),
currency: orders[0]?.currency?.isoCode || 'EUR'
});
});
Shopware Plugins: Internal Logic, Full Power
Plugins, unlike apps, run inside the Shopware core and have access to everything – services, database, events, etc.
Let’s modify our GMV example:
We’ll create a plugin that recognizes requests made by our app – and manipulates the data being returned.
public function onOrderLoaded(EntityLoadedEvent $event): void
{
$source = $event->getContext()->getSource();
if (!$source instanceof AdminApiSource) return;
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'FakeGmvReporting'));
$apps = $this->appRepository->search($criteria, $event->getContext())->getEntities();
$app = $apps->first();
if ($app && $source->getIntegrationId() === $app->getIntegrationId()) {
foreach ($event->getEntities() as $entity) {
if (!$entity instanceof OrderEntity) continue;
$entity->setAmountTotal(1.00);
$entity->setAmountNet(0.84);
$entity->setShippingTotal(0.00);
$entity->setTaxStatus('gross');
}
}
}
🤯 The plugin detects API requests made by the app and changes the order data on the fly!
Summary
Shopware App | Shopware Plugin | |
---|---|---|
Runs where? | External server | Inside the Shopware environment |
Deployment | No deployment on Shopware required | Installed as plugin |
Security | Needs explicit permissions | Has full access |
Use Cases | External services, integrations | Deep customizations |
Example | Read orders via Admin API | Modify data before it's returned |
Need Help with Your Own Shopware App or Plugin?
If you're planning to develop your own Shopware 6 app or plugin and could use some expert support, I’m happy to help.
From figuring out the Shopware API to building a clean plugin structure — I’ve got your back.
You can find more info about my services here: Shopware 6 plugin development.