Prerequisites
You need:- ✅ A JTL ID (your login to the JTL ecosystem)
- ✅ Access to an organization (tenant) in the Partner Portal (created automatically on first login)
- ✅ Node.js v24.16.0 or higher (current LTS, includes
npm). Verify withnode --versionandnpm --version
What you’re Building
Before writing code, here’s how a JTL Cloud App works: Your frontend runs inside JTL’s App Shell (in an iframe). It communicates with the shell through AppBridge, a small SDK that exposes shell capabilities to the iframe. The most important one isgetAppToken, a short-lived signed token that proves which merchant (tenant) is currently using your app. Your frontend sends that token to your backend, which verifies it and uses it to make tenant-scoped calls to the JTL API.
Not every page in your app runs inside the shell. The frontend has three routes, and they fall into two groups:
/setupand/erprun inside the App Shell iframe. AppBridge is available, and the app knows which tenant it’s serving./hubopens in a standalone browser tab when a merchant clicks the app card. There is no iframe, no AppBridge, and no tenant context.
Working with an AI coding tool? Connect the JTL docs MCP server,
then paste the prompt below to scaffold the project before following the steps here.
AI Assistant Prompt
1. Create the Project
- Ok to proceed? (y):
y - Install with npm and start now?:
Yes
Ctrl + C to stop the dev server.
2. Install Packages
3. Configure Vite
Replacefrontend/vite.config.ts:
- TanStack Router plugin watches
src/routes/and regeneratessrc/routeTree.gen.tswhenever you add or rename a route file. The plugin must be listed beforereact(). See the installation docs for details. - Tailwind plugin enables Tailwind utilities, which the JTL UI library depends on.
- Dev proxy forwards
/api/*requests to a backend onlocalhost:5273. Your frontend can callfetch('/api/...')and Vite will route the request without CORS issues during development.
4. Set up Styles
Replacefrontend/src/index.css:
5. Create the Route Files
TanStack Router uses file-based routing, so your folder structure undersrc/routes/ is your route configuration. Delete the default src/App.tsx (the router replaces it), then create the following structure:
__root.tsxis the top-level layout, always rendered._shell.tsxis a pathless layout route. The leading underscore means it doesn’t add a URL segment, so_shell.setup.tsxbecomes/setup, not/_shell/setup. Any route file prefixed with_shell.shares the layout’s wrapper.hub.tsxdoesn’t, so it bypasses the AppBridge initialization entirely.
Root Route
Add the following to yourfrontend/src/routes/__root.tsx:
<Outlet /> is where child routes render. During development, a devtools panel appears in the bottom corner. It won’t be included in your production build.
Shell Layout (AppBridge Provider)
First, create afrontend/src/appBridge.tsx file that initializes AppBridge once, exposes the bridge through React Context, and shows a loading state until the connection completes.
frontend/src/routes/_shell.tsx:
Setup Page
Add the following to yourfrontend/src/routes/_shell.setup.tsx:
useAppBridge() hook to get the app token and send it to the /api/connect-tenant backend endpoint. The backend verifies the tenantId and completes the tenant connection. Once the app has successfully finished setup, call appBridge.method.call('setupCompleted') to mark the setup as complete.
If your app has additional setup steps, such as onboarding users, collecting merchant information, or completing internal checks, you can include those steps before calling setupCompleted.
ERP Page
Add the following to yourfrontend/src/routes/_shell.erp.tsx:
Hub Page
This route sits outside the shell layout and doesn’t inherit AppBridge initialization. When a merchant clicks the app card in the JTL Hub, this page opens in a full browser tab with no iframe and no app token.frontend/src/routes/hub.tsx:
6. Wire up the Router and Add AppBridgeProvider
Updatefrontend/src/main.tsx to initialize the router and render the layout and routes.
7. Run the Frontend
Start the dev server:src/routeTree.gen.ts automatically.
Visit each route to confirm the app works:
1
Visit http://localhost:5173/setup
You should see a spinner and then a message: “Connecting to JTL Platform…”. This shows the routing works and the shell layout mounts, AppBridge attempts to initialize, and fails (no backend yet).
2
Visit http://localhost:5173/erp
Same waiting message as before. Confirms the second iframe route loads and reuses the shell layout.
3
Visit http://localhost:5173/hub
You should see the “Launched from JTL Cloud” card. No spinner, no error, since this route bypasses the shell layout entirely.
Common Issues
'window is not defined' or 'AppBridge is not defined'
'window is not defined' or 'AppBridge is not defined'
This app is a client-only Vite build with no server render, so a top-level import is safe. Make sure you are running
npm run dev rather than building for a server environment.If you are adapting this setup for a framework with server-side rendering, load the SDK with a dynamic await import('@jtl-software/cloud-apps-core') inside a client-side provider instead.'Cannot find module ./routeTree.gen' in main.tsx
'Cannot find module ./routeTree.gen' in main.tsx
This means the route tree file has not been generated yet. It is created automatically by the TanStack Router Vite plugin when it detects your route files.Start the dev server with
npm run dev, then save any file inside src/routes/. The plugin watches this folder and will generate src/routeTree.gen.ts as soon as it detects a change.If the file still does not appear, check vite.config.ts. The tanstackRouter() plugin must be listed before react() in the plugins array.Routes resolve to /_shell/setup instead of /setup
Routes resolve to /_shell/setup instead of /setup
This usually means the route is not being treated as a pathless layout.In TanStack Router, the leading underscore (
_) marks a route as pathless. If the file is named shell.setup.tsx (without the underscore), _shell becomes part of the URL.Rename the file to _shell.setup.tsx to restore the expected behavior. Also make sure you are using the flat-file convention (_shell.setup.tsx), not a folder structure like _shell/setup.tsx, since this guide assumes the flat-file format.useAppBridge() returns null inside a child route, even after the spinner finishes
useAppBridge() returns null inside a child route, even after the spinner finishes
This usually happens when
AppBridgeContext and useAppBridge are defined in the same file as a TanStack Router route (for example, _shell.tsx).During development, the router’s Vite integration can create multiple module instances under hot module replacement (HMR). This causes the provider and consumer to reference different context objects, so useAppBridge() returns null.Fix: Move the context, hook, and provider into a separate file (for example, src/appBridge.tsx) and import useAppBridge from there in all components.Next: Build the Backend
Pick a language to build the backend that will verify app tokens and proxy requests to the JTL API:Node.js
Express and TypeScript.
C# (.NET)
ASP.NET Core 8.
PHP
Slim 4 and PHP.