Shopware Apps vs. Plugins – Real-Life Example with Code
Stefan Pilz

Stefan Pilz @stefan-freelancer

About: 💻 Freelance Shopware & E-Commerce Developer 🚀 I help online businesses build high-performing, customized Shopware stores. 🎯 Focus: shop migrations, plugin development, performance optimization

Location:
Paphos, Cyprus
Joined:
Mar 27, 2025

Shopware Apps vs. Plugins – Real-Life Example with Code

Publish Date: Apr 10
0 0

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:

<?xml version="1.0" encoding="UTF-8"?>
<manifest 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">
<meta>
    <name>FakeGmvReporting</name>
    <label>Fake GMV Reporting</label>
    <label lang="de-DE">Fake GMV Reporting</label>
    <description>Test app for reading orders to calculate GMV.</description>
    <author>Friendly Hacker</author>
    <version>1.0.0</version>
    <license>MIT</license>
</meta>

<setup>
    <registrationUrl>http://fake-gmv-app:3000/register</registrationUrl>
    <secret>supersecret</secret>
</setup>

<webhooks>
    <webhook name="appActivated" url="http://fake-gmv-app:3000/activated" event="app.activated"/>
</webhooks>

<permissions>
    <read>order</read>
    <read>order_line_item</read>
    <read>currency</read>
    <read>system_config</read>
</permissions>
</manifest>
Enter fullscreen mode Exit fullscreen mode

✅ 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'
  });
});
Enter fullscreen mode Exit fullscreen mode

/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);
});
Enter fullscreen mode Exit fullscreen mode

⚠️ 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);
});
Enter fullscreen mode Exit fullscreen mode

ℹ️ 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'
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🤯 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.

Comments 0 total

    Add comment