A Java Spring Boot library for building Shopware 6 App backends.
Unlike a Shopware Plugin, a Shopware App cannot directly extend or modify the core functionality of a Shopware shop using PHP code. If the desired features cannot be fully realized with App Scripts or Storefront templates and JavaScript, the app must rely on the shop’s REST APIs and webhooks to implement its features.
Such a Shopware App is therefore essentially a server application, the App Backend.
This library provides the core components needed to build a Shopware App Backend:
- 🔄 App Registration & Lifecycle - Registration, confirmation, and app lifecycle event handling
- 🔐 Authentication & Security - Shop request signature verification and response signing
- 🌐 API Client - Pre-configured client for the Shopware Admin API
- 📊 Event Handling - Webhook processing and event dispatching
A Shopware App with a backend consists of two parts:
┌─────────────────────────────────────────────────────────────────────────────┐
│ SHOPWARE SHOP │
│ │
│ ┌─────────────────────┐ │
│ │ Your App │ (installed from zipped manifest.xml) │
│ │ - manifest.xml │ │
│ └─────────┬───────────┘ │
│ │ │
└────────────┼────────────────────────────────────────────────────────────────┘
│ HTTP calls (registration, webhooks, actions)
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ YOUR APP BACKEND │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ App Server Library │ ───► │ Your Business Code │ │
│ │ (this library) │ │ (ShopwareApp impl) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
-
The Shopware App is a ZIP file containing at least a
manifest.xml. It defines your app's metadata, permissions, webhooks, and action buttons. The manifest points to your backend's URLs. -
The App Backend (built with this library) receives HTTP requests from Shopware when:
- A shop installs your app (registration)
- Events occur that your app subscribed to (webhooks)
- Users click action buttons you defined
The App Server library handles the security handshake, verifies request signatures, manages OAuth tokens, and routes requests to your code.
- Java 17 or later
- Gradle 8+ or Maven
Gradle:
dependencies {
implementation 'de.codebarista:shopware-app-server:1.0.0'
}Maven:
<dependency>
<groupId>de.codebarista</groupId>
<artifactId>shopware-app-server</artifactId>
<version>1.0.0</version>
</dependency>See the basic-app example for a complete working application demonstrating app registration, webhooks, action buttons, and Admin API usage.
The App Server library can host multiple App Backends within a single service. Each app is identified by its App Key, which corresponds to a subdomain.
Each app runs on its own subdomain. The subdomain (everything before the first dot) serves as the App Key.
For example, with app key my-app and domain example.com:
- Registration URL:
https://my-app.example.com/shopware/api/v1/registration/register
Your ShopwareApp implementation must return this app key from getAppKey().
Create a Spring Bean implementing the ShopwareApp interface:
@Component // <-- This is important!
public class MyShopwareApp implements ShopwareApp {
@Override
public String getAppKey() {
return "my-app"; // Must match subdomain
}
@Override
public String getAppSecret() {
return "my-secret"; // Matches <secret> in manifest.xml (dev only)
}
@Override
public String getAppName() {
return "MyShopwareApp"; // Matches <name> in manifest.xml
}
@Override
public void onEvent(ShopwareEventDto event, long internalShopId,
Locale userLocale, String shopwareLanguageId) {
// Handle webhook events
}
@Override
public ActionResponseDto<?> onAction(ActionRequestDto action, long internalShopId,
Locale userLocale, String shopwareLanguageId) {
// Handle action button clicks
return null;
}
}The App Server automatically registers these endpoints and secures them with signature verification.
Every request from Shopware includes a shopware-shop-signature header containing an HMAC-SHA256 hash of the request
body, signed with the shared secret (your app secret during registration, or the shop's secret after confirmation).
The App Server automatically:
- Extracts the signature from the header
- Computes the expected signature from the request body
- Rejects requests where signatures don't match
This ensures requests genuinely come from Shopware and haven't been tampered with. You don't need to implement this
yourself—it's handled by the ShopwareSignatureVerificationFilter.
| Endpoint | Description |
|---|---|
GET /shopware/api/v1/registration/register |
Called when a shop installs your app; returns credentials for the handshake. Calls the onRegisterShop() or onReRegisterShop() method of your ShopwareApp implementation. |
POST /shopware/api/v1/registration/confirm |
Called by Shopware to confirm the registration and exchange API credentials |
| Endpoint | Description |
|---|---|
POST /shopware/api/v1/action |
Receives action button clicks. Handle them in the onAction() method of your ShopwareApp implementation. |
POST /shopware/api/v1/event |
Receives webhook events your app subscribed to in manifest.xml. Handle them in the onEvent() method of your ShopwareApp implementation. |
Lifecycle events notify your backend when the app is activated, deactivated, updated, or deleted in a shop. These events are opt-in—Shopware only sends them if you register for them in your manifest.
Note: Currently, only the
deletedevent has built-in handling (marks it as deleted in the database). The other events are received and logged only! See GitHub Issue.Workaround: Lifecycle events are standard webhook events you can subscribe to. Route them to the
/shopware/api/v1/eventendpoint (see Events section above) and handle them there.
You need to define the URLs listed below in your manifest to use the app-server's Lifecycle controller.
| Endpoint | Description |
|---|---|
POST /shopware/api/v1/lifecycle/activated |
Called when the shop admin activates your app |
POST /shopware/api/v1/lifecycle/deactivated |
Called when the shop admin deactivates your app |
POST /shopware/api/v1/lifecycle/updated |
Called when your app is updated to a new version |
POST /shopware/api/v1/lifecycle/deleted |
Called when your app is uninstalled from a shop. Marks it as deleted in the database. Then calls the onDeleteShop() method of your ShopwareApp implementation. |
See the Shopware documentation for more details:
| Endpoint | Description |
|---|---|
GET /shopware/admin/{folder}/{version}/index.html |
Serves your custom Administration UI as an iframe |
Note: Custom endpoints you add require their own security implementation. See App Backend Security.
app-server:
ssl-only: true
map-localhost-ip-to-localhost-domain-name: false
http-request-response-logging-enabled: false
database:
user-migrations: false| Property | Default | Description |
|---|---|---|
ssl-only |
true |
When enabled, rejects shop URLs that don't use HTTPS. Shopware Cloud always uses HTTPS, so this protects against misconfigured on-premise installations or man-in-the-middle attacks. Disable only for local development. |
map-localhost-ip-to-localhost-domain-name |
false |
Maps 127.0.0.1 to localhost in incoming requests. Required for local development because Shopware sends the IP while your app expects the hostname. |
http-request-response-logging-enabled |
false |
Logs full HTTP request/response bodies for outgoing Admin API calls. Useful for debugging but verbose—don't enable in production. |
database.user-migrations |
false |
Controls how Liquibase migrations run. See Database Migrations for details. |
For local development, you typically need:
app-server:
ssl-only: false # Allow HTTP (Shopware local dev uses HTTP)
map-localhost-ip-to-localhost-domain-name: true # Fix localhost/127.0.0.1 mismatch
http-request-response-logging-enabled: true # See what's happening (optional)Production Checklist: Set
ssl-only: true,map-localhost-ip-to-localhost-domain-name: false, and consider disabling logging before deploying!
The App Server needs a database to store registered shops.
Out of the box, the App Server provides an in-memory SQLite database. This is perfect for a first start but data is lost when the application restarts.
Configure a persistent database using standard Spring Boot properties:
spring:
datasource:
url: jdbc:sqlite:shopware_apps.db # File-based SQLite
driver-class-name: org.sqlite.JDBC
hikari:
maximum-pool-size: 1 # SQLite supports only one writer
connection-timeout: 5000
data-source-properties:
foreign_keys: true # Enable FK constraints
busy_timeout: 5000 # Wait for locks instead of failing
journal_mode: WAL # Better concurrency
jpa:
database-platform: org.hibernate.community.dialect.SQLiteDialect
hibernate:
ddl-auto: none # Liquibase manages the schemaYou can also use PostgreSQL, MySQL, or any database supported by Spring Data JPA.
The App Server uses Liquibase for schema management. Understanding how migrations work helps you decide whether you need custom tables.
When your app starts, the App Server automatically:
- Creates its own
SpringLiquibasebean - Runs migrations from
db/changelog/app-server-changelog-master.xml(bundled in the library) - Creates the
SHOPWARE_SHOPtable for storing shop registrations
This is all you need if you're only handling webhooks and actions without storing custom data.
If your app needs its own database tables you take control of migrations:
- Set the flag:
app-server:
database:
user-migrations: true-
The App Server now skips creating its Liquibase bean, allowing you to define your own.
-
Create your master changelog and include the App Server migrations first:
<!-- src/main/resources/db/changelog/my-changelog-master.xml -->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" ...>
<!-- REQUIRED: App Server tables (SHOPWARE_SHOP) must be created first -->
<include file="db/changelog/app-server-changelog-master.xml"/>
<!-- Your custom tables come after -->
<include file="changesets/0001-my-custom-table.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>- Point Spring to your changelog:
spring:
liquibase:
change-log: classpath:db/changelog/my-changelog-master.xmlWhy include App Server migrations? The SHOPWARE_SHOP table is required for the library to function. By including
app-server-changelog-master.xml, you ensure it's created before your tables (important if you have foreign keys
referencing it).
See the custom-db-migration and db-shop-reference-example for working examples.
The manifest.xml is the heart of your Shopware App. It's a configuration file that tells Shopware:
- What your app is called and who made it
- Where your backend is hosted (URLs)
- What permissions your app needs
- Which events your app wants to receive
- What action buttons to add to the Administration
You package this file into a ZIP (optionally with assets) and upload it to a Shopware shop or the Shopware Store.
The <setup> section tells Shopware where to register when your app is installed:
<manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/trunk/src/Core/Framework/App/Manifest/Schema/manifest-1.0.xsd">
<meta>
<name>MyShopwareApp</name> <!-- Must match getAppName() in your ShopwareApp -->
<label>My Shopware App</label>
<author>Your Company</author>
<version>1.0.0</version>
<license>MIT</license>
</meta>
<setup>
<registrationUrl>https://my-app.example.com/shopware/api/v1/registration/register</registrationUrl>
<secret>my-secret</secret>
</setup>
</manifest>About the <secret> tag:
- For local development: Include it and return the same value from
getAppSecret()in your code - For production (Shopware Store): Remove it—Shopware generates and manages the secret automatically
- Never commit real secrets to version control
Webhooks let your app react to events in Shopware. When something happens (order created, product updated, etc.), Shopware sends an HTTP POST to your backend.
<webhooks>
<webhook name="orderWritten"
url="https://my-app.example.com/shopware/api/v1/event"
event="order.written"/>
<webhook name="productWritten"
url="https://my-app.example.com/shopware/api/v1/event"
event="product.written"/>
</webhooks>| Attribute | Description |
|---|---|
name |
Your identifier for this webhook (used in logs) |
url |
Always use the /shopware/api/v1/event endpoint |
event |
The Shopware event to subscribe to (see Event Reference) |
Handle events in your ShopwareApp implementation:
@Override
public void onEvent(ShopwareEventDto event, long internalShopId,
Locale userLocale, String shopwareLanguageId) {
String eventName = event.data().event(); // e.g., "order.written"
JsonNode payload = event.data().payload(); // Event-specific data
switch (eventName) {
case "order.written" -> processOrder(payload, internalShopId);
case "product.written" -> syncProduct(payload, internalShopId);
}
}Action buttons add custom actions to the Shopware Administration. Users click them and your backend responds.
<admin>
<action-button action="processOrder"
url="https://my-app.example.com/shopware/api/v1/action"
entity="order" view="detail">
<label>Process Order</label>
<label lang="de-DE">Bestellung verarbeiten</label>
</action-button>
</admin>| Attribute | Description |
|---|---|
action |
Your identifier for this action (received in onAction()) |
url |
Always use the /shopware/api/v1/action endpoint |
entity |
Which entity type this button appears on (order, product, customer, etc.) |
view |
Where the button appears: list, detail, or both |
Handle actions in your ShopwareApp implementation:
@Override
public ActionResponseDto<?> onAction(ActionRequestDto action, long internalShopId,
Locale userLocale, String shopwareLanguageId) {
String actionName = action.data().action(); // e.g., "processOrder"
List<String> ids = action.data().ids(); // Selected entity IDs
if ("processOrder".equals(actionName)) {
processOrders(ids, internalShopId);
return ActionResponseDto.notification(
NotificationResponseDto.success("Processed " + ids.size() + " orders!")
);
}
return null;
}Response types you can return:
ActionResponseDto.notification(...)- Show a toast notificationActionResponseDto.modal(...)- Open a modal with custom contentActionResponseDto.reload(...)- Reload the current page
Returning null results in HTTP 401 (Unauthorized). Use this as a fallback for unhandled actions.
The App Server can serve custom Administration UI pages.
Configure the base URL in your manifest:
<admin>
<base-app-url>https://{app-key}.example.com/shopware/admin/{admin-extension-folder}/v1/index.html</base-app-url>
</admin>The admin-extension-folder in the manifest must match the return value of the getAdminExtensionFolderName() method
of your ShopwareApp.
Place your HTML files in src/main/resources/public/shopware/admin/{admin-extension-folder}/{version}/index.html.
The admin extension folder can contain multiple versions of the UI. Each version folder must contain the respective index.html and required content. The version defined in the manifest will be served.
The App Server can inject these attributes into the HTML before serving:
| Placeholder | Injected Value |
|---|---|
data-token="" |
Authentication token (validate with TokenService) |
data-version="" |
Value from ShopwareApp#getVersion(), or empty string if null. Intended to expose the backend version to UI. |
See App Backend Security for how to validate tokens in your endpoints.
See Shopware Meteor Admin SDK for more information.
Your backend can push notifications to the Shopware Administration at any time (not just in response to actions). First, request permission:
<permissions>
<create>notification</create>
<permission>notification:create</permission>
</permissions>Then use the AdminApi to send notifications:
@Autowired
private AdminApi adminApi;
void notifyTest() {
adminApi.pushSuccessMessage(myApp, shopId, "Operation completed!");
adminApi.pushErrorMessage(myApp, shopId, "Something went wrong");
}The App Server handles security differently depending on who is calling your endpoints.
Requests from Shopware to the App Server's built-in endpoints (registration, webhooks, actions) are automatically
secured via HMAC-SHA256 signature verification. You don't need to do anything—this is handled by
ShopwareSignatureVerificationFilter.
See the Shopware documentation on Signing & Verification in the App System.
If your app has an Admin Extension UI (served as an iframe in Shopware Administration), that UI runs in the user's browser. When it calls your custom backend endpoints, those requests come from the browser—not from Shopware—so they don't have Shopware's signature.
For these requests, you need to implement your own authentication. The App Server helps by injecting a token into your HTML.
When Shopware loads your Admin Extension, it requests /shopware/admin/{folder}/{version}/index.html from your backend.
Before serving the file, the App Server can inject authentication tokens into your HTML.
The token is valid for one hour for the app that requested it.
Step 1: Add placeholder attributes to your HTML
In your index.html, add empty data-token attribute:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Admin Extension</title>
</head>
<body data-token="">
<div id="app"></div>
<script src="main.js"></script>
</body>
</html>Step 2: The App Server injects the token
When serving this file, the App Server replaces the empty value:
data-token="" → data-token="01234asdf..."
Step 3: Your JavaScript reads the token
const token = document.body.dataset.token;
const version = document.body.dataset.version;
// Include token in API calls to your backend
fetch('/api/my-endpoint?shopId=' + shopId + '&token=' + token)The injected token can be validated with the TokenService.
Option 1: Using @PreAuthorize (Recommended)
@RestController
@RequestMapping("/api")
public class MyApiController {
@GetMapping("/my-endpoint")
@PreAuthorize("@tokenService.isAppTokenValid(@myShopwareApp, #shopId, #token)")
public ResponseEntity<?> myEndpoint(
@RequestParam("shopId") String shopId,
@RequestParam("token") String token) {
// Token is valid if we get here
return ResponseEntity.ok("Success");
}
}The @PreAuthorize annotation:
@tokenService- References theTokenServicebean@myShopwareApp- References yourShopwareAppbean by name (the class name in camelCase)#shopId,#token- References the method parameters
For @PreAuthorize to work, enable method security in your application:
@EnableMethodSecurity
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}Option 2: Programmatic validation
@Autowired
private TokenService tokenService;
@Autowired
private MyShopwareApp myShopwareApp;
@GetMapping("/my-endpoint")
public ResponseEntity<?> myEndpoint(
@RequestParam("shopId") String shopId,
@RequestParam("token") String token) {
if (!tokenService.isAppTokenValid(myShopwareApp, shopId, token)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// Token is valid
return ResponseEntity.ok("Success");
}Note: Avoid using the
shopwareRestTemplatedirectly. Use theAdminApiinstead.
The App Server provides a pre-configured RestTemplate:
@Autowired
@Qualifier("shopwareRestTemplate")
private RestTemplate shopwareRestTemplate;Features: disabled redirects, buffered requests, enhanced error handling, optional request/response logging.
| Example | Description |
|---|---|
| basic-app | Complete app with webhooks, action buttons, and Admin API |
| custom-db-migration | Custom database tables with Liquibase |
| db-shop-reference-example | Foreign key relationships to SHOPWARE_SHOP |
This project follows Semantic Versioning 2.0.0.
- MAJOR - Breaking changes
- MINOR - New features (backwards compatible)
- PATCH - Bug fixes
See CHANGELOG.md for release history.
MIT License
Contributions welcome! Please submit a Pull Request.
- GitHub Issues - Bug reports and feature requests
- GitHub Discussions - Questions and community help