{"slug": "six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect", "title": "Six Cloud Apps, Six Patterns: What We Learned Building Live at JTL Connect", "summary": "A team at JTL Academy Day in Cologne built six working JTL Cloud Apps live during a 90-minute session, using Claude Code to turn audience-suggested business ideas into apps inside Cloud ERP. The apps — including Bestandsradar, first-order-welcome, Kundenrisiko, Wetter-Sport, hand-build-app, and Profit Pulse — demonstrate patterns ranging from context-aware Panels and .NET backends to external weather API integration and financial data aggregation, with source code and prompt histories published in the jtl-software/academy-day-examples repository.", "body_md": "At JTL Academy Day in Cologne, we wanted to answer a simple question: **What can you actually build with JTL Cloud Apps?**\n\nWe didn't want to answer that with a product demo or a hypothetical architecture. We wanted to start with real business ideas and turn them into working apps inside Cloud ERP.\n\nDuring a 90-minute session titled *\"Kein Entwickler? Kein Problem!\"* (No developer? No problem!), the audience called out app ideas while we used Claude Code to build them. We registered the apps, opened them inside Cloud ERP, and tested them live.\n\nAcross the two conference days, six apps came out of that experiment:\n\nThe source code, screenshots, and prompt histories are available in the [academy-day-examples](https://github.com/jtl-software/academy-day-examples) repository.\n\nThese aren't production applications. We deliberately kept them small so we could build and test them during the session. But that also makes them useful examples of how different parts of the JTL Cloud Platform fit together, from dashboards and Panels to ERP API access, backend services, external data, data aggregation, and different backend languages.\n\nMore importantly, they show that a JTL Cloud App doesn't have to be another generic dashboard sitting beside your ERP. It can become part of the workflow your customers already use.\n\nEach app started with a business problem, not a technical requirement.\n\n| **App** | **What it does** | **Where it appears** | **Key pattern** | \n| **Bestandsradar** | Suggests which items to reorder and when | Cloud ERP menu | Dashboard with backend | \n| **first-order-welcome** | Recognizes a customer's first order and drafts a welcome email | Sales order Panel | Context-aware Panel | \n| **Kundenrisiko** | Scores a customer's returns risk | Customer Panel + Cloud ERP menu | Panel with .NET backend | \n| **Wetter-Sport** | Combines sales history with weather data to suggest reorders | Item Panel + Cloud ERP menu | ERP data + external API | \n| **hand-build-app** | Calculates the actual purchase value of goods sold | Customer Panel + Cloud ERP menu | Order data aggregation | \n| **Profit Pulse** | Breaks revenue and gross margin down by supplier, customer, and delivery address | Cloud ERP menu | Financial data aggregation | \n\nThe interesting part isn't just what each app does. It's how different the ideas are while using the same JTL Cloud App building blocks.\n\nSome apps live in the Cloud ERP menu, while others appear alongside a customer, item, or sales order. Some work with JTL data alone; others combine it with external APIs or process larger amounts of data in a backend service.\n\n**The platform provides the building blocks. The business problem determines what you build with them.**\n\nFive of the six apps started from the official JTL Cloud App template. This gives developers a consistent starting point, whether they're building a dashboard, a Panel, or an app that needs a backend service.\n\nThe basic workflow looks like this:\n\n`npx @jtl-software/create-cloud-app@latest`.` npm run register`.` npm run dev`.\nThe template provides the project structure, frontend, optional backend, and manifest that describes the app to Cloud ERP. From there, you can adapt the structure to the problem you're solving rather than setting up the entire application from scratch.\n\nThe manifest is particularly important because it defines the app's footprint in the platform. For example, **Bestandsradar** defines a configuration page, a menu item, the API scopes it needs, and its authentication configuration:\n\n```\n{\n  \"version\": \"1.0.0\",\n  \"technicalName\": \"bestandsradar-e354dd\",\n  \"lifecycle\": {\n    \"configurationUrl\": \"http://localhost:3024/setup\"\n  },\n  \"capabilities\": {\n    \"hub\": {\n      \"appLauncher\": { \"redirectUrl\": \"http://localhost:3024/hub\" }\n    },\n    \"erp\": {\n      \"menuItems\": [\n        { \"id\": \"bestandsradar-menu\", \"name\": \"Bestandsradar\", \"url\": \"http://localhost:3024/erp\" }\n      ],\n      \"api\": {\n        \"scopes\": [\"items.read\", \"salesorders.read\"]\n      }\n    }\n  },\n  \"authentication\": {\n    \"publicClient\": {\n      \"redirectUris\": [\"http://localhost:3024/callback\"],\n      \"postLogoutRedirectUris\": [\"http://localhost:3024/\"]\n    },\n    \"serviceAccount\": {\n      \"clientCredentials\": true,\n      \"description\": \"bestandsradar backend service account\"\n    }\n  }\n}\n```\n\nThere isn't much platform-specific configuration hidden away elsewhere. The manifest tells JTL what the app needs and where it should appear.\n\n`configurationUrl` is the setup page the JTL Hub opens during installation.`menuItems` puts an entry in the Cloud ERP's `/erp` in an iframe.`api.scopes` is the data the app may read. Two scopes, both read-only, because a reorder dashboard needs items and sales orders and nothing else.`serviceAccount` lets the backend get its own access token with client credentials.\nThe `localhost` URLs are fine while you develop. Before anyone else installs the app, every URL in the manifest has to point to a real HTTPS host.\n\nBestandsradar answers a question every merchant asks on Monday morning: *what should I reorder, and by when?* It opens from the **Apps **section under** Third-party Apps > Bestandsradar** in Cloud ERP and shows the best sellers, a list of items about to run out, and a suggested order quantity for each.\n\nThe interesting part isn't the dashboard itself. It's what happens behind it. The frontend gets an app token through AppBridge and passes it to the backend. The backend verifies the token, extracts the tenant from its claims, and uses its service account to query the ERP API.\n\nThat gives you a useful pattern for larger JTL Cloud Apps: **ERP interface → app backend → JTL API**\n\nThe backend can combine API calls, run business logic, or process larger amounts of data. Tenant handling matters here, too, because the backend should derive the tenant from the verified token rather than trust a value supplied by the browser.\n\nBestandsradar also keeps its reorder calculation separate from the API layer. That makes the logic easier to test and reuse, including in the demo route, which runs against sample data.\n\nA dashboard is useful when someone wants to review information. But what if they’re already working on a customer, order, or item? That’s where a Panel comes in. It sits alongside the Cloud ERP’s own screens and shows relevant context for whatever the user has selected, so they can get the information they need without leaving the screen they’re working on.\n\nThree apps use Panels to bring information into the existing Cloud ERP workflow:\n\nThe user doesn't have to open another application or copy an ID somewhere else. The app follows the context they're already working in.\n\nFor example, a Panel can subscribe to changes in the selected customer:\n\n``` js\nuseEffect(() => {\n  const unsubscribe = appBridge.event.subscribe(\n    'CustomerChanged',\n    (data: unknown) => {\n      setCustomerId((data as { customerId: string }).customerId);\n    }\n  );\n\n  appBridge.method\n    .call('getCurrentCustomerId')\n    .then((id) => setCustomerId(id as string))\n    .catch(() => undefined);\n\n  return () => unsubscribe();\n}, [appBridge]);\n```\n\nThe same idea applies to other contexts, such as sales orders and items. The Panel can retrieve the currently selected record and react when the user switches to another one.\n\nThis is one of the patterns worth taking beyond the demo. A JTL Cloud App doesn't always need to replace an existing ERP workflow. Sometimes the better experience is to **add useful context to the existing workflow**.\n\nNot every app needs a server. The `first-order-welcome` Panel takes the app token from AppBridge and uses it directly to query the ERP GraphQL API:\n\n```\nconst { accessToken } =\n  await appBridge.method.call<{ accessToken: string }>('getAppToken');\n\nconst client = new GraphQLClient(`${jtlApiUrl}/erp/v2/graphql`, {\n  headers: {\n    Authorization: `Bearer ${accessToken}`\n  }\n});\n```\n\nFor a small Panel that reads a few records about the thing currently on screen, this can be a simpler architecture. There's no backend to host or additional server-side proxy between the Panel and Cloud ERP.\n\nThat doesn't mean frontend-only is always the right answer. A backend becomes useful when an app needs secrets, third-party API credentials, shared caching, or heavier processing.\n\nStart with the simplest architecture that fits the problem, and add a backend when the problem needs one.\n\nWetter-Sport started with a simple observation from a sports shop owner: **my sales depend on the weather**.\n\nAn ERP contains sales history, products, stock, and orders. It doesn't know whether tomorrow will be sunny, so the app combines ERP data with weather information from [Open-Meteo](https://open-meteo.com).\n\nFor an item, the Panel shows the weather on the days it sold. The dashboard uses historical sales and a seven-day forecast to suggest what might need to be reordered before the weekend. The app turns raw weather data into a few simple categories:\n\n```\nexport function classify(\n  tempMax: number,\n  precipitation: number,\n  sunshineHours: number\n): WeatherClass {\n  if (precipitation >= 1) return 'rain';\n  if (tempMax < 8) return 'cold';\n  if (tempMax >= 18 || (tempMax >= 14 && sunshineHours >= 6)) {\n    return 'sunny';\n  }\n\n  return 'mixed';\n}\n```\n\nFrom there, the app compares sales behaviour across those weather classes and uses the forecast to estimate demand.\n\nThe same pattern could combine ERP data with shipping information, marketing data, exchange rates, public APIs, machine-learning models, or data from another system your customers already use. The Cloud App becomes the place where those sources meet.\n\nThere was also a useful lesson in what didn't work immediately. The weather archive had a delay for recent dates, so some of the latest sales initially had no weather data. The solution was to combine the archive with the forecast endpoint. That's the sort of edge case that tends to appear when an idea moves from a demo to something people rely on.\n\nTwo apps tackle money questions, which means adding up a lot of line items. `hand-build-app` calculates the actual purchase value of everything sold. `profit-pulse` breaks down revenue and gross margin by supplier, customer, and delivery address, with monthly trends and CSV export.\n\nBoth face the same challenge: totals live on line items, which you must fetch separately for each order or invoice. A hundred orders can mean a hundred requests. Send them all at once, and you risk hammering the API. Send them one at a time, and the dashboard takes forever to load.\n\nBoth apps use controlled concurrency when fetching data for many orders or invoices. `profit-pulse`, for example, keeps five requests running at a time rather than sending every request simultaneously:\n\n``` js\nasync function parallelMap(values, limit, fn) {\n  const results = new Array(values.length);\n  let next = 0;\n\n  await Promise.all(\n    Array.from(\n      { length: Math.min(limit, values.length) },\n      async () => {\n        while (next < values.length) {\n          const index = next++;\n          results[index] = await fn(values[index]);\n        }\n      }\n    )\n  );\n\n  return results;\n}\n\nconst invoiceItems = await parallelMap(valid, 5, (invoice) =>\n  jtlGet(`/sales-invoices/${encodeURIComponent(invoice.salesInvoiceId)}/line-items`, context),\n);\n```\n\nThe lesson is broader than this particular implementation. A dashboard that works perfectly with ten orders can behave very differently with thousands, so think about concurrency, caching, and API limits early.\n\nThe same care applies to the numbers you're showing. `hand-build-app` calculates actual purchase value rather than using the item's average purchase price. Profit Pulse uses the item's current default supplier and excludes returns, shipping costs, and overheads from its gross margin calculation.\n\nWhen an app shows financial information, the calculation needs to be as clear as the UI. **Say what the number means, and what it doesn't mean.**\n\nKundenrisiko takes another approach. Its backend runs on .NET 8 rather than Node.js, but the frontend doesn't need to care. It sends a Bearer token and receives a JSON response.\n\nThe .NET implementation uses Microsoft's token validation libraries to retrieve the issuer configuration and signing keys, then validates the token before processing the request.\n\nFrom the frontend's perspective, none of that changes. A JTL Cloud App can use Node.js, .NET, or PHP while keeping its application interface consistent.\n\nThe finished apps are only half of the story. Every app in the repository also keeps its prompt history, so you can see not just what was built, but how the AI assistant got there.\n\n**Give the assistant the platform context first.** Kundenrisiko started with a request to build and register an app that scores customer return risk. The assistant initially took the wrong approach to registration. Follow-up prompts asked it to use the CLI and consult the JTL Developer Docs. Once the JTL Developer Docs MCP server was connected, the assistant had the context it needed to follow the official workflow. AI can make building faster, but it still needs the right context.\n\n**Review the manifest like you would review code.** One prompt in `hand-build-app` asked the assistant to expand the application's scopes arbitrarily. It requested more permissions than the final app needed, and the manifest was later trimmed back. Review scopes and authentication configuration as carefully as you would any other code in a pull request.\n\n**Say whether you're building a demo or a real integration.** Kundenrisiko initially used mock data. Asking for real data exposed a limitation: the GraphQL API didn't provide the general returns query the original risk model expected. Mock data can prove that a UI works, but it can't tell you whether the platform has the data your business logic needs. Ask the assistant to use real APIs early when building an actual integration.\n\n**Make the AI's decisions visible.** For `wetter-sport`, the assistant chose the weather provider, default location, historical window, and thresholds for identifying weather-sensitive products. Those decisions were documented in the README. You don't need to make every technical decision yourself, but you should be able to see the decisions the assistant made and change them later.\n\n**Keep your development setup predictable.** Port conflicts came up repeatedly in the prompt histories. Local URLs are part of the app manifest, so changing ports can mean updating and registering the app again. Pick your ports early and keep them stable.\n\nGetting an app running inside the Cloud ERP is the exciting part. Shipping it to merchants is where the engineering work really begins. Before turning one of these experiments into a production application:\n\n`localhost` URL in the manifest for the real one.`.gitignore` already covers `.env` and `appsettings.Local.json`. Keep it that way.\nThe fastest way to learn these patterns is to run one of the apps and change something. Clone the [academy-day-examples repo](https://github.com/jtl-software/academy-day-examples), pick the app closest to your idea, and start from its demo route.\n\nWhen you're ready to build your own:\n\nEvery app in the repo started as a one-sentence idea from someone in the audience. **What's yours?**", "url": "https://wpnews.pro/news/six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect", "canonical_source": "https://dev.to/jtl-softwareeng/six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect-dm1", "published_at": "2026-10-16 14:40:29+00:00", "updated_at": "2026-10-16 14:47:17.425738+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["JTL", "JTL Cloud Apps", "Cloud ERP", "Claude Code", "JTL Academy Day", "Bestandsradar", "Profit Pulse", "Cologne"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect", "markdown": "https://wpnews.pro/news/six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect.md", "text": "https://wpnews.pro/news/six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect.txt", "jsonld": "https://wpnews.pro/news/six-cloud-apps-six-patterns-what-we-learned-building-live-at-jtl-connect.jsonld"}}