Embedding a Private ToolJet Application
ToolJet allows you to securely embed your private applications inside portals, dashboards, or third-party systems without requiring your users to log in again.
Embedded private apps continue to respect all permissions and workspace access controls, ensuring that users only see the data and features they are authorized for. Each embedded session is scoped and isolated, so access is controlled per user per application without interfering with other ToolJet sessions.
Key Benefits:
- Secure by default: Private apps remain inaccessible without authenticated sessions
- No login friction: Embedded apps load seamlessly inside iframes without redirects
- Fine-grained access control: Sessions inherit existing app and workspace permissions
- Session isolation: Embedded usage does not interfere with primary ToolJet sessions
- Backend-controlled access: Authentication and session lifecycle are fully managed server-side
When to Use Private App Embedding
- Embed sesitive ToolJet apps inside customer portals or internal dashboards
- Integrate ToolJet workflows into existing SaaS products or admin systems
- Expose sensitive operational tools without making them public
- Reuse ToolJet apps as a secure internal tool layer within your platform
Authentication Flow
- User logs into your portal using your preferred authentication method.
- Your backend generates a personal access token (PAT) scoped to that user and app.
- Backend returns an embed URL containing the PAT.
- Frontend renders the ToolJet app inside an iframe using the embed URL.
- The embedded session is scoped and isolated, ensuring that the user has access only to the intended app without affecting other ToolJet sessions.
See the diagram below for a detailed view of the authentication and embedding flow.
Steps to Embed a Private Application
-
Configure Your Frontend: Your frontend needs to request an embedded app URL from your backend immediately after the user logs in. The same email used for login will be used to generate the ToolJet embed URL.
- HTML/JS
- React
- Vue.js
- Angular
<!DOCTYPE html>
<html>
<body>
<div id="app-container">
<p>Loading your dashboard...</p>
</div>
<script>
// This function runs after user successfully logs in
async function onUserLogin(userEmail) {
// App ID of the ToolJet app you want to embed
const appId = '8ba8bf0e-6b8f-4e07-abb9-6fd2d816fabc';
try {
// Request the embed URL from your backend
const response = await fetch('/api/get-tooljet-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userEmail,
appId: appId
})
});
const data = await response.json();
if (data.redirectUrl) {
// Create and insert iframe
const iframe = document.createElement('iframe');
iframe.src = data.redirectUrl;
iframe.width = '100%';
iframe.height = '600px';
iframe.style.border = 'none';
document.getElementById('app-container').innerHTML = '';
document.getElementById('app-container').appendChild(iframe);
}
} catch (error) {
console.error('Error loading ToolJet app:', error);
document.getElementById('app-container').innerHTML =
'<p>Failed to load dashboard. Please refresh the page.</p>';
}
}
// Call this after your authentication succeeds
// Example: after successful login response
// onUserLogin('[email protected]');
</script>
</body>
</html>import React, { useState, useEffect } from 'react';
import { useAuth } from './AuthContext'; // Your auth context
function Dashboard() {
const { user } = useAuth(); // Get logged-in user info
const [embedUrl, setEmbedUrl] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Load ToolJet app as soon as user is available
if (user?.email) {
loadToolJetApp(user.email);
}
}, [user]);
const loadToolJetApp = async (userEmail) => {
const appId = '8ba8bf0e-6b8f-4e07-abb9-6fd2d816fabc';
try {
const response = await fetch('/api/get-tooljet-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userEmail,
appId: appId
})
});
const data = await response.json();
setEmbedUrl(data.redirectUrl);
} catch (error) {
console.error('Error loading ToolJet app:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading your dashboard...</div>;
return (
<div>
<h1>Welcome, {user?.name}</h1>
{embedUrl && (
<iframe
src={embedUrl}
width="100%"
height="600px"
style={{ border: 'none' }}
title="ToolJet App"
/>
)}
</div>
);
}
export default Dashboard;<template>
<div>
<div v-if="loading">Loading your dashboard...</div>
<div v-else-if="error">{{ error }}</div>
<iframe
v-else-if="embedUrl"
:src="embedUrl"
width="100%"
height="600px"
style="border: none"
title="ToolJet App"
/>
</div>
</template>
<script>
export default {
name: 'ToolJetEmbed',
props: {
userEmail: {
type: String,
required: true
}
},
data() {
return {
embedUrl: null,
loading: true,
error: null
};
},
mounted() {
// Load ToolJet app immediately when component mounts
this.loadToolJetApp();
},
methods: {
async loadToolJetApp() {
const appId = '8ba8bf0e-6b8f-4e07-abb9-6fd2d816fabc';
try {
const response = await fetch('/api/get-tooljet-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: this.userEmail,
appId: appId
})
});
if (!response.ok) {
throw new Error('Failed to load ToolJet app');
}
const data = await response.json();
this.embedUrl = data.redirectUrl;
} catch (error) {
console.error('Error loading ToolJet app:', error);
this.error = 'Failed to load dashboard. Please refresh the page.';
} finally {
this.loading = false;
}
}
}
};
</script>import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-tooljet-embed',
template: `
<div *ngIf="loading">Loading your dashboard...</div>
<div *ngIf="error">{{ error }}</div>
<iframe
*ngIf="embedUrl"
[src]="embedUrl"
width="100%"
height="600px"
style="border: none"
title="ToolJet App"
></iframe>
`
})
export class ToolJetEmbedComponent implements OnInit {
@Input() userEmail!: string;
embedUrl: SafeResourceUrl | null = null;
loading = true;
error: string | null = null;
constructor(
private http: HttpClient,
private sanitizer: DomSanitizer
) {}
ngOnInit() {
// Load ToolJet app immediately when component initializes
this.loadToolJetApp();
}
async loadToolJetApp() {
const appId = '8ba8bf0e-6b8f-4e07-abb9-6fd2d816fabc';
try {
const response: any = await this.http.post('/api/get-tooljet-url', {
email: this.userEmail,
appId: appId
}).toPromise();
// Sanitize the URL for security
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(response.redirectUrl);
} catch (error) {
console.error('Error loading ToolJet app:', error);
this.error = 'Failed to load dashboard. Please refresh the page.';
} finally {
this.loading = false;
}
}
} -
Configure Your Backend: Your backend is responsible for securely calling the ToolJet API to generate a Personal Access Token and return the embed URL.
- Node.js/Express
- Python/Flask
- PHP/Laravel
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// Your ToolJet configuration
const TOOLJET_HOST = 'https://your-tooljet-instance.com'; // or http://localhost:3000
const TOOLJET_API_TOKEN = 'your_basic_auth_token_here'; // Keep this secret!
app.post('/api/get-tooljet-url', async (req, res) => {
const { email, appId } = req.body;
// Validate the request
if (!email || !appId) {
return res.status(400).json({ error: 'Email and appId are required' });
}
try {
// Call ToolJet API to generate PAT
const response = await axios.post(
`${TOOLJET_HOST}/api/ext/users/personal-access-token`,
{
email: email,
appId: appId,
sessionExpiry: 60, // Session valid for 60 minutes
patExpiry: 3600 // Token valid for 1 hour (3600 seconds)
},
{
headers: {
'Authorization': `Basic ${TOOLJET_API_TOKEN}`,
'Content-Type': 'application/json'
}
}
);
// Return the redirect URL to frontend
res.json({
redirectUrl: response.data.redirectUrl
});
} catch (error) {
console.error('ToolJet API Error:', error.response?.data || error.message);
// Handle specific error cases
if (error.response?.status === 404) {
return res.status(404).json({ error: 'User not found in ToolJet' });
}
if (error.response?.status === 403) {
return res.status(403).json({ error: 'User does not have access to this app' });
}
if (error.response?.status === 429) {
return res.status(429).json({ error: 'Too many requests. Please try again later.' });
}
res.status(500).json({ error: 'Failed to generate embed URL' });
}
});
app.listen(3001, () => {
console.log('Server running on port 3001');
});from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
# Your ToolJet configuration
TOOLJET_HOST = 'https://your-tooljet-instance.com' # or http://localhost:3000
TOOLJET_API_TOKEN = 'your_basic_auth_token_here' # Keep this secret!
@app.route('/api/get-tooljet-url', methods=['POST'])
def get_tooljet_url():
data = request.get_json()
email = data.get('email')
app_id = data.get('appId')
# Validate the request
if not email or not app_id:
return jsonify({'error': 'Email and appId are required'}), 400
try:
# Call ToolJet API to generate PAT
response = requests.post(
f'{TOOLJET_HOST}/api/ext/users/personal-access-token',
json={
'email': email,
'appId': app_id,
'sessionExpiry': 60, # Session valid for 60 minutes
'patExpiry': 3600 # Token valid for 1 hour (3600 seconds)
},
headers={
'Authorization': f'Basic {TOOLJET_API_TOKEN}',
'Content-Type': 'application/json'
}
)
response.raise_for_status()
# Return the redirect URL to frontend
return jsonify({
'redirectUrl': response.json()['redirectUrl']
})
except requests.exceptions.HTTPError as e:
status_code = e.response.status_code
# Handle specific error cases
if status_code == 404:
return jsonify({'error': 'User not found in ToolJet'}), 404
elif status_code == 403:
return jsonify({'error': 'User does not have access to this app'}), 403
elif status_code == 429:
return jsonify({'error': 'Too many requests. Please try again later.'}), 429
return jsonify({'error': 'Failed to generate embed URL'}), 500
except Exception as e:
print(f'Error: {str(e)}')
return jsonify({'error': 'Failed to generate embed URL'}), 500
if __name__ == '__main__':
app.run(port=3001)<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class ToolJetController extends Controller
{
public function getToolJetUrl(Request $request)
{
// Validate the request
$validated = $request->validate([
'email' => 'required|email',
'appId' => 'required|string',
]);
$tooljetHost = env('TOOLJET_HOST', 'https://your-tooljet-instance.com');
$tooljetApiToken = env('TOOLJET_API_TOKEN'); // Keep this in .env file
try {
// Call ToolJet API to generate PAT
$response = Http::withHeaders([
'Authorization' => 'Basic ' . $tooljetApiToken,
'Content-Type' => 'application/json',
])->post($tooljetHost . '/api/ext/users/personal-access-token', [
'email' => $validated['email'],
'appId' => $validated['appId'],
'sessionExpiry' => 60, // Session valid for 60 minutes
'patExpiry' => 3600 // Token valid for 1 hour (3600 seconds)
]);
if ($response->failed()) {
$statusCode = $response->status();
// Handle specific error cases
if ($statusCode === 404) {
return response()->json(['error' => 'User not found in ToolJet'], 404);
} elseif ($statusCode === 403) {
return response()->json(['error' => 'User does not have access to this app'], 403);
} elseif ($statusCode === 429) {
return response()->json(['error' => 'Too many requests. Please try again later.'], 429);
}
return response()->json(['error' => 'Failed to generate embed URL'], 500);
}
// Return the redirect URL to frontend
return response()->json([
'redirectUrl' => $response->json()['redirectUrl']
]);
} catch (\Exception $e) {
\Log::error('ToolJet API Error: ' . $e->getMessage());
return response()->json(['error' => 'Failed to generate embed URL'], 500);
}
}
} -
Dynamically Update the iframe: The iframe is automatically created and loaded once the redirectUrl is received from your backend. Use conditional rendering to display the iframe only when the URL is available.
- JavaScript
- React
- Vue.js
- Angular
function displayToolJetApp(redirectUrl) {
const container = document.getElementById('app-container');
if (redirectUrl) {
const iframe = document.createElement('iframe');
iframe.src = redirectUrl;
iframe.width = '100%';
iframe.height = '600px';
iframe.style.border = 'none';
iframe.title = 'ToolJet Application';
// Clear loading message and add iframe
container.innerHTML = '';
container.appendChild(iframe);
}
}{embedUrl && (
<iframe
src={embedUrl}
width="100%"
height="600px"
style={{ border: 'none' }}
title="ToolJet Application"
/>
)}<iframe
v-if="embedUrl"
:src="embedUrl"
width="100%"
height="600px"
style="border: none"
title="ToolJet Application"
/><iframe
*ngIf="embedUrl"
[src]="embedUrl"
width="100%"
height="600px"
style="border: none"
title="ToolJet Application"
></iframe>