firebase/php-jwt for app token verification. By the end, the backend runs locally and the frontend’s “Connecting to JTL Platform…” placeholder turns into a real connection.
Stack: PHP 8.1+, Slim 4, Guzzle for HTTP requests, vlucas/phpdotenv for environment variables, firebase/php-jwt for JWT verification.
Prerequisites
You need:- ✅ A finished frontend from the Build the Frontend page, running locally on
http://localhost:5173 - ✅ PHP 8.1 or higher. Run
php --versionto check. - ✅ Composer for dependency management. Run
composer --versionto check.
What you’re Building
During setup, your backend verifies that the app token from your frontend is valid and was issued by JTL for your app. To do this:- Your backend fetches the identity provider’s public keys (JWKS)
- Verifies the token’s signature, issuer, and expiry against them
- Confirms the token’s audience is your app, so a token minted for another app is rejected
1. Set up the Project
Create abackend folder alongside the existing frontend folder, then initialise a Composer project inside it.
2. Install Packages
3. Set up Environment Variables
Createbackend/.env:
/api/* requests to localhost:5273, so the PORT value matches that target. JTL_APP_ID is your app’s ID, which an app token’s audience must contain. JTL_ISSUER is the identity provider that issues app tokens.
Add .env and vendor/ to backend/.gitignore so secrets and dependencies don’t end up in version control:
4. Build the Auth Helper
The first piece of the backend is a function that authenticates with JTL using your client credentials and returns an access token. Your backend uses it to make tenant-scoped calls to the JTL Cloud API. Createbackend/src/JtlAuth.php:
client_credentials grant to JTL’s auth endpoint. The access token is cached in memory until 30 seconds before expiry, so repeat calls within the same request reuse it.
5. Build the Token Verifier
The backend verifies the app token before trusting anything in it.firebase/php-jwt parses the identity provider’s key set and selects the key matching the token’s kid.
Create backend/src/AppTokenVerifier.php:
JWT::decode verifies the signature against the key whose kid matches the token header, and checks the expiry. The issuer and audience checks are separate because the library does not enforce them.
The urn:jtl:app_id claim is optional. When absent the token is valid for your app, and when present it must equal your app ID.
The key set is fetched on first use and held on the instance, so repeat verifications within the same request do not refetch. For cross-request reuse, swap the in-memory cache for APCu or another shared cache.
6. Build the Connect Tenant Endpoint
Now connect the verifier into a Slim route. The frontend’s shell layout sends the app token to the backend as a Bearer token. Createbackend/public/index.php:
.env, instantiates JtlAuth with the credentials and AppTokenVerifier with the issuer and app ID. Both instances are then captured by the route closure via use ($verifier) and shared across requests.
The CORS middleware allows requests from the Vite dev server on port 5173. The dev proxy in vite.config.ts already routes frontend fetch('/api/...') calls to this backend, but the CORS headers are a useful safety net during development.
See Tenant Mapping for more on managing tenants in production.
7. Configure Composer Autoloading
Openbackend/composer.json and add a psr-4 autoload mapping for the App\ namespace:
App\ namespace lives under the src/ directory, which is how JtlAuth and SessionVerifier get loaded when index.php references them.
8. Run the Backend
Start the dev server:401 response with {"error":"Failed to verify app token"}. That’s the expected outcome for an invalid token. A real app token from the App Shell will follow the same path and succeed.
Common Issues
'Class App\JtlAuth not found' or similar autoload error
'Class App\JtlAuth not found' or similar autoload error
This error means Composer’s autoloader doesn’t know where to find the class. The most common cause is forgetting to regenerate the autoload files after adding the
psr-4 mapping to composer.json.Running composer dump-autoload from the backend/ directory rebuilds vendor/autoload.php to include any new namespace mappings. The error should clear after the next request.'CLIENT_ID and CLIENT_SECRET must be defined in .env'
'CLIENT_ID and CLIENT_SECRET must be defined in .env'
This error means the backend started but couldn’t find your credentials in the environment. The most common cause is that
.env is sitting in the wrong directory, or that the Dotenv::createImmutable() call is pointing at the wrong path.'Failed to verify app token' with real credentials
'Failed to verify app token' with real credentials
Verification checks the signature, issuer, expiry, and audience. The audience check is the one most often wrong:
JTL_APP_ID is the app ID shown in the Partner Portal under your app, not the client ID.If the audience is correct, confirm JTL_ISSUER matches the environment your app is registered in. A token issued by one environment does not verify against another’s keys.App tokens are valid for about an hour, so a token captured earlier during debugging will fail on expiry.'Failed to fetch JWT (401)' from the auth endpoint
'Failed to fetch JWT (401)' from the auth endpoint
A 401 from the auth endpoint means the credentials are not valid.If you are still using placeholder values, this is expected. Real credentials are provided after registering your app in the Partner Portal.If you have already registered:
- check for typos or extra spaces in
.env - restart the dev server after making changes
.env require a restart.CORS error in the browser console
CORS error in the browser console
A CORS error usually means the request reached the backend but the browser blocked the response because the origin didn’t match what the backend allows. The CORS middleware in
index.php is configured to allow http://localhost:5173, which matches the default Vite dev server port.If the Vite dev server is running on a different port (for example, because port 5173 was already in use and Vite picked 5174 instead), the browser will see a mismatch. Updating the Access-Control-Allow-Origin value in index.php to match the actual Vite port, or freeing up port 5173, should resolve it.Next: Connect and Fetch Data
The backend verifies app tokens and is ready to call the JTL Cloud API. The remaining work is registering your app with JTL to get real credentials, installing the app in the JTL Hub, and pulling product data from JTL-Wawi:Connect and Fetch Data
Register your app, install it in the JTL Hub, and fetch products from
JTL-Wawi.