ToolJet: Build Internal Tools Without Writing Full-Stack Code
Internal tools — admin panels, inventory dashboards, support queues, customer lookup apps — usually fall into a frustrating category: too custom for off-the-shelf software, but not important enough to justify building from scratch. ToolJet exists to solve that problem.
Photo by Erik Mclean on Unsplash
It's an open-source, self-hosted low-code platform where you drag UI components onto a canvas, connect them to databases or APIs, and wire up actions with JavaScript. No backend code, no frontend build system, no deployment pipeline — just a working internal app.
How ToolJet Works
The development model:
- Connect data sources — PostgreSQL, MySQL, MongoDB, REST APIs, Google Sheets, Stripe, S3, and dozens more
- Build the UI — drag tables, forms, charts, buttons, and inputs onto a canvas
- Write queries — SQL or API requests that your components trigger
- Add logic — JavaScript for transformations, conditionals, and chaining queries
- Deploy — share a URL with your team; ToolJet handles auth
Everything runs inside your ToolJet instance. Credentials for databases and APIs stay on your server.
Docker Deployment
The quickest path is Docker Compose:
# Download the official compose file
curl -LO https://tooljet-deployments.s3.us-west-1.amazonaws.com/docker/docker-compose.yaml
# Configure environment
cp .env.example .env
# Edit .env: set TOOLJET_HOST, SECRET_KEY_BASE, LOCKBOX_MASTER_KEY
# Generate values: openssl rand -hex 32
docker compose up -d
ToolJet needs a PostgreSQL database. The Compose file includes one, or you can point PG_HOST at an existing Postgres instance.
For production:
services:
tooljet-server:
image: tooljet/tooljet:latest
environment:
TOOLJET_HOST: https://tools.yourdomain.com
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
LOCKBOX_MASTER_KEY: ${LOCKBOX_MASTER_KEY}
PG_HOST: postgres
PG_DB: tooljet_production
PG_USER: tooljet
PG_PASS: ${PG_PASSWORD}
depends_on:
- postgres
restart: unless-stopped
tooljet-client:
image: tooljet/tooljet:latest
command: ["npm", "run", "serve:client"]
ports:
- "80:8082"
restart: unless-stopped
postgres:
image: postgres:15
environment:
POSTGRES_DB: tooljet_production
POSTGRES_USER: tooljet
POSTGRES_PASSWORD: ${PG_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pg_data:
Connecting Data Sources
After setup, go to Data Sources → Add new. For PostgreSQL:
- Select PostgreSQL
- Enter host, port, database, username, password
- Click Test connection
- Save
ToolJet stores credentials encrypted using your LOCKBOX_MASTER_KEY. You can also use SSH tunnels for databases not exposed to the internet.
Supported natively:
- SQL databases: PostgreSQL, MySQL, MariaDB, MSSQL, SQLite
- NoSQL: MongoDB, Redis, Elasticsearch, Firestore
- Cloud: AWS S3, BigQuery, DynamoDB, Redshift
- SaaS: Stripe, Airtable, GitHub, Slack, HubSpot, Salesforce
- REST/GraphQL APIs
Building an App: Customer Lookup
A practical example: a support team needs to look up customers by email and see their recent orders.
Step 1: Add a query
In the Query Editor, add a PostgreSQL query named getCustomer:
SELECT id, name, email, plan, created_at
FROM customers
WHERE email = {{textinput1.value}}
LIMIT 1
Add another query getOrders:
SELECT order_id, product, amount, status, created_at
FROM orders
WHERE customer_id = {{queries.getCustomer.data[0].id}}
ORDER BY created_at DESC
LIMIT 20
Step 2: Build the UI
Drag onto the canvas:
TextInput(for email search)Button(trigger search)Table(display customer info)Table(display orders)
Step 3: Wire it up
- Button
onClick→ rungetCustomer→ on success, rungetOrders - Customer table data source:
{{queries.getCustomer.data}} - Orders table data source:
{{queries.getOrders.data}}
The {{}} syntax is JavaScript — you can use any JS expression to transform data before it reaches a component.
JavaScript Transformations
Between queries and UI, you can transform data with JavaScript:
// In a query's "Transform" tab:
// Calculate days since signup
return data.map(customer => ({
...customer,
days_since_signup: Math.floor(
(new Date() - new Date(customer.created_at)) / (1000 * 60 * 60 * 24)
)
}));
This runs client-side — no additional server code needed.
Access Control
ToolJet has workspace-level roles (Admin, Developer, Viewer) and app-level permissions. Viewers can use apps but not edit them. Apps can also be shared via a public URL (with or without authentication required).
For more granular control, ToolJet Enterprise adds SSO and group-based permissions.
ToolJet vs. Retool
| Feature | ToolJet | Retool |
|---|---|---|
| Self-hostable | ✓ | ✓ (paid) |
| Open source | ✓ (AGPL) | ✗ |
| Pricing (cloud) | Free tier | $10/user/month |
| Data sources | 40+ | 60+ |
| Component library | Good | Excellent |
| Mobile support | Beta | ✓ |
Retool has a more polished component library and better mobile support. ToolJet wins on cost (free, self-hostable) and the fact that the source code is auditable.
When to Use ToolJet
ToolJet is a good fit when:
- You need an admin panel or internal dashboard in days, not weeks
- You want to give non-developers access to database data safely
- You're tired of writing CRUD interfaces for every new data source
- You need to keep credentials on-premises
Skip it for customer-facing products (it's optimized for internal use) or when you need custom components that go beyond the provided widget library.
