Build a Custom Dashboard with Retool (Better Than Tableau)
Tableau starts at $75 per Creator per month. Build internal dashboards with Retool directly on your database, in the cloud or self-hosted, without ETL and without vendor lock-in.
Tableau charges $75 per Creator per month (billed annually) and is a pure analytics tool without forms or write actions. Custom dashboards in React need a development team.
Retool lets you build internal tools with drag-and-drop components that work directly on your database. No ETL, cloud or self-hosted, free for up to 5 users.
Why Tableau Gets Expensive for Ops Teams
Your operations team needs real-time dashboards for orders, inventory and customer support. Tableau is a strong analytics tool for that, but it has a price: in the Standard edition a Creator costs $75 per user per month, an Explorer $42 and a Viewer $15, each billed annually. Every deployment needs at least one Creator. And Tableau remains a tool for looking at data: forms, buttons and write actions on your database are not what it is built for.
Through our Dashboards service we build internal dashboards for clients that do both: show data and change data. The result is a tool your ops team can adapt themselves without needing an analyst for every new view.
Unlike Tableau dashboards, Retool connects directly to your production database through a read-only user. No data copying needed.
The Modern Dashboard Stack: Retool + PostgreSQL + REST APIs
Here’s the architecture we use in our custom development implementations:
Why Retool?
- Drag-and-drop UI builder: No React/Vue coding required
- Native database connectors: PostgreSQL, MySQL, MongoDB, Snowflake and more
- REST API integration: Connect to Stripe, Shopify, HubSpot directly
- Access control: Permission groups and access levels per app
- Self-hosted option: Run on your infrastructure, on every plan
Cost comparison (official price lists, as of September 2026):
- Tableau Standard, billed annually: Creator $75, Explorer $42, Viewer $15 per user per month. Example with 10 users, 2 Creators and 8 Explorers: $150 + $336 = $486/month ($5,832/year)
- Retool Team, billed annually: €9 per builder and €5 per internal user per month. Example with 10 users, 2 builders and 8 internal users: €18 + €40 = €58/month (€696/year)
- Retool Free: €0 for up to 5 users, unlimited apps
Retool shows its pricing page in the currency of your location, Tableau lists in US dollars with a currency converter. That is why we do not convert one into the other here. Check both pages before deciding, prices change.
Step 1: Set Up Retool (Cloud or Self-Hosted)
Option A: Retool Cloud (fastest)
- Go to retool.com and sign up
- The Free plan covers up to 5 users with unlimited web and mobile apps
- Skip this section
What else the Free plan limits: 500 workflow runs per month, 5 GB of storage in the Retool Database, up to 5 modules and 250 AI credits per month. From the sixth user on you need the Team plan. A staging environment and app releases start with Team, audit logs and custom permission groups with Business, SAML/OIDC SSO and source control are Enterprise only. All figures from the pricing page.
Option B: Self-Hosted
Self-hosting is available on every plan, including Free. You need Enterprise for unlimited users, SSO and advanced self-hosting features (pricing page, Self-host tab). You get the licence key from the self-hosted portal at my.retool.com.
For tests and proofs of concept, Docker Compose on a Linux VM is enough. Retool lists as the minimum Ubuntu 22.04 or newer, 8 vCPUs, 64 GiB RAM and 60 GiB storage and states explicitly that Docker deployments are not suitable for production use. For production, Kubernetes with Helm is the officially supported configuration.
The Docker variant in four commands:
git clone https://github.com/tryretool/retool-onpremise.git && cd retool-onpremise
./install.sh
sudo docker compose up -d
sudo docker compose ps
Between ./install.sh and the start you set three values in docker.env: LICENSE_KEY with your key from the portal, BASE_DOMAIN with the full URL of your instance (http://localhost:3000 for local tests) and COOKIE_INSECURE=true as long as you test without HTTPS. The script also generates an ENCRYPTION_KEY. Back it up, it cannot be recovered. Afterwards you reach Retool at http://localhost:3000/auth/signup and create the first admin. The Compose file ships a PostgreSQL container for Retool’s own database.
Pro tip: Self-hosting does not replace the user fees. You still pay the plan per builder and internal user, plus your own infrastructure. The gain is elsewhere: your data and the Retool instance stay inside your network.
Step 2: Connect Your Data Sources
Retool’s killer feature: connect to any data source without ETL.
Connect PostgreSQL:
- In Retool, go to Resources → Create new → Resource and select the PostgreSQL tile (guide)
- Enter connection details:
Host: your-db.aws.rds.amazonaws.com Port: 5432 Database name: production Username: retool_readonly Password: [secure password] - Enable SSL/TLS and click Test connection → Save
If you use Retool Cloud, your database must accept connections from the Retool IP addresses. Retool offers three outbound regions for that: us-west-2 (default), eu-central-1 (Frankfurt) and ap-southeast-1 (outbound regions). Important for privacy questions: according to Retool, the outbound region does not change where Retool stores your apps and data. If you need real data residency, self-host.
CRITICAL: Use read-only credentials
Create a dedicated user in PostgreSQL that can only read. The last statement makes sure tables created later are readable automatically:
CREATE USER retool_readonly WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE production TO retool_readonly;
GRANT USAGE ON SCHEMA public TO retool_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO retool_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO retool_readonly;
Connect REST APIs (Stripe, Shopify, etc.):
- Go to Resources → Create new → Resource → REST API
- Enter base URL:
https://api.stripe.com/v1 - Add authentication:
- Type: Bearer Token
- Token: a restricted key with the prefix
rk_live_and read permissions only. Stripe recommends restricted keys over the secret keysk_live_because they carry only the permissions you grant
- Save as “Stripe API”
Need help connecting custom integrations? We can set up secure API gateways with rate limiting.
Step 3: Build Your First Dashboard (Order Management)
Let’s build a real-time order dashboard. This is the most common use case we see at our dashboard implementation service.
One note first: in 2026 Retool introduced a new AI-powered app builder where you describe apps in a prompt and edit the React code directly. The previous drag-and-drop interface is now called “Classic apps” and remains available. This guide uses Classic apps because every component and every property is documented and traceable there. Retool explains the differences in its comparison of both approaches.
Dashboard features:
- Real-time order list with filters (status, date range, customer)
- Order details panel (line items, shipping, customer info)
- Quick actions (mark shipped, refund, email customer)
- Revenue metrics (daily, weekly, monthly trends)
Create a New App
- In Retool, click Create → App and choose the Classic variant
- Name it “Order Dashboard”
- Choose the desktop layout
Add the Order List (Table Component)
- Drag the Table component onto the canvas
- Rename to
orderTable - Click New Query → Select the “PostgreSQL” resource
- Write query
getOrders:
SELECT
o.id,
o.order_number,
o.created_at,
o.status,
o.total_price,
o.currency,
c.email AS customer_email,
c.first_name || ' ' || c.last_name AS customer_name,
COUNT(oi.id) AS item_count
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE
o.created_at >= {{ startDate.value }}
AND o.created_at <= {{ endDate.value }}
AND ({{ statusFilter.value }} = 'all' OR o.status = {{ statusFilter.value }})
GROUP BY o.id, c.id
ORDER BY o.created_at DESC
LIMIT 100;
Retool sends SQL queries as prepared statements by default. Every {{ }} expression is passed as a parameter, never pasted into the query as text. That protects against SQL injection, but it also means you cannot set table and column names dynamically this way.
- Set Data source for the table:
{{ getOrders.data }} - Configure columns (columns guide):
- Hide the
idcolumn created_at: In the Inspector under Format choose the type Datetimetotal_price: Choose the format Currency and set the currencystatus: Format Tag with a colour per value (green = fulfilled, yellow = pending, red = cancelled)- For custom formats,
currentRowis available in mapped values, for example{{ moment(currentRow.created_at).format('MMM D, YYYY h:mm A') }}. Moment.js is preloaded in Retool
- Hide the
Add Filters
Date range:
- Drag two Date components above the table
- Rename to
startDateandendDate - Set defaults:
{{ moment().subtract(7, 'days').format('YYYY-MM-DD') }}and{{ moment().format('YYYY-MM-DD') }} - Leave the run behaviour of the
getOrdersquery on Automatic. It then reruns as soon as a referenced value changes (run behavior)
We use two single Date components instead of the Date Range component because their value is a plain string according to the reference. That keeps the query readable and avoids surprises with the structure of the range value.
Status filter:
- Drag a Select dropdown
- Rename to
statusFilter - Set options:
[ { label: 'All Orders', value: 'all' }, { label: 'Pending', value: 'pending' }, { label: 'Fulfilled', value: 'fulfilled' }, { label: 'Cancelled', value: 'cancelled' } ] - Default value:
'all'
Add Order Details Panel
- Drag a Container to the right side of the screen
- Rename to
orderDetailsPanel - Set visibility:
{{ orderTable.selectedRow !== null }} - Inside the container, add Text components:
Order #{{ orderTable.selectedRow.order_number }}
Customer: {{ orderTable.selectedRow.customer_name }}
Email: {{ orderTable.selectedRow.customer_email }}
Status: {{ orderTable.selectedRow.status }}
Total: {{ orderTable.selectedRow.total_price }} {{ orderTable.selectedRow.currency }}
- Add a Table for line items:
- Create query
getOrderItems:
- Create query
SELECT
product_name,
quantity,
price,
quantity * price AS subtotal
FROM order_items
WHERE order_id = {{ orderTable.selectedRow.id }};
- Set table data:
{{ getOrderItems.data }}
Add Quick Actions
Mark as Shipped button:
- Drag a Button into the details panel
- Label: “Mark as Shipped”
- Click → New Query → Type: PostgreSQL, run behaviour Manual
- Query
markShipped:
UPDATE orders
SET
status = 'fulfilled',
fulfilled_at = NOW()
WHERE id = {{ orderTable.selectedRow.id }}
RETURNING *;
This query needs write permissions. Create a second resource with a user that may only UPDATE on orders instead of giving the read-only user more rights.
- In the query’s Event handlers tab add a handler for the Success event: action Control query →
getOrders→ Trigger, so the table reloads (event handlers) - Add a second Success handler with Show notification:
"Order #{{ orderTable.selectedRow.order_number }} marked as shipped"
Send Email button:
- Add another Button
- Label: “Email Customer”
- Click → New Query → Type: REST API
- Create a SendGrid resource first: base URL
https://api.sendgrid.com/v3, auth Bearer Token with your SendGrid API key. If you use an EU subuser at SendGrid, the base URL ishttps://api.eu.sendgrid.com/v3 - Create query
sendEmailwith method POST and path/mail/send(Mail Send API). Body as JSON:
{
"personalizations": [{
"to": [{ "email": "{{ orderTable.selectedRow.customer_email }}" }],
"subject": "Your order #{{ orderTable.selectedRow.order_number }} has been updated"
}],
"from": { "email": "support@yourstore.com" },
"content": [{
"type": "text/plain",
"value": "Hi {{ orderTable.selectedRow.customer_name }}, the status of your order is now: {{ orderTable.selectedRow.status }}"
}]
}
Want automated email workflows instead of manual clicks? Check our workflow automation service.
Step 4: Add Revenue Metrics (Charts)
Ops teams need to see trends, not just raw data.
Daily Revenue Chart:
- Drag a Line Chart component above the table. The older component called “Chart” is legacy according to Retool, use the newer chart components (charts guide)
- Create query
getDailyRevenue:
SELECT
DATE(created_at) AS date,
SUM(total_price) AS revenue,
COUNT(*) AS order_count
FROM orders
WHERE
created_at >= {{ startDate.value }}
AND created_at <= {{ endDate.value }}
AND status != 'cancelled'
GROUP BY DATE(created_at)
ORDER BY date ASC;
- Configure chart:
- Data source:
{{ getDailyRevenue.data }} - X-axis: column
date - Y-axis: column
revenue - Title: “Daily Revenue”
- Data source:
Metric Cards (KPIs):
- Drag 3 Statistic components side by side
- Configure:
- Total Orders:
{{ getOrders.data.length }} - Total Revenue:
{{ getOrders.data.reduce((sum, row) => sum + row.total_price, 0) }}with format Currency - Avg Order Value:
{{ getOrders.data.reduce((sum, row) => sum + row.total_price, 0) / getOrders.data.length }}with format Currency
- Total Orders:
Step 5: Add Access Control (Role-Based Permissions)
Different teams need different access levels.
Create user groups:
- Go to Settings → Groups (guide)
- Create groups:
ops_team(view + edit orders)support_team(view only)admin(full access)
According to Retool, custom groups and per-app access are only available from the Business plan on. On Free and Team only the default groups “All Users” and “Admin” exist.
Set app permissions:
- Open the group and assign the app with one of the three levels (permission levels): Use (run the app, no editor), Edit (build and release the app) or Own (additionally manage sharing)
- Set:
ops_team: Usesupport_team: Use, additionally hide the “Mark as Shipped” button for this groupadmin: Own
Hide sensitive data:
The current_user object returns the groups of the signed-in user as a list of objects with id and name. In the customer email column, set this as the mapped value:
{{ current_user.groups.map(g => g.name).includes('admin') ? currentRow.customer_email : '***@***' }}
Put the same expression (negated) into the Hidden property of the shipping button so support_team does not see it.
Need enterprise-grade security controls? We implement SSO, audit logs and field-level encryption. Keep in mind: according to the pricing page Retool offers SAML and OIDC SSO on Enterprise only, the setup is described in the SSO quickstart.
Step 6: Connect Live APIs (Stripe, Shopify, etc.)
Most dashboards need data from multiple sources.
Add Stripe payment data:
- Create query
getStripePaymentswith the Stripe resource, method GET, path:
/charges?limit=100&created[gte]={{ moment(startDate.value).unix() }}
Stripe’s charges list accepts a limit between 1 and 100 (default 10) and filters with created[gte] by Unix timestamp in seconds. The response returns the charges in the data field and tells you with has_more whether you need to paginate with starting_after.
- Display in a separate table
Join with order data:
Create a JavaScript query joinPayments:
const orders = getOrders.data;
const payments = getStripePayments.data.data;
return orders.map(order => ({
...order,
stripe_charge_id: payments.find(p => p.metadata.order_id === String(order.id))?.id,
payment_method: payments.find(p => p.metadata.order_id === String(order.id))?.payment_method_details?.type
}));
This assumes your checkout writes the order ID into metadata.order_id when creating the charge. Stripe stores metadata as strings, hence the comparison with String(order.id).
Production Deployment Checklist
Before sharing with your team:
- Test with read-only DB user: Never use admin credentials
- Add error handling: A Failure handler with a readable error message for every query
- Check query timeouts: Retool cancels queries after 10 seconds, on Retool Cloud you can raise that to at most 120 seconds, self-hosted via environment variable. Queries that take longer need optimising
- Add loading states: Show spinners while data loads
- Optimize queries: Add indexes on filtered columns (created_at, status)
- Set up monitoring: Track query performance and error rates
- Document for ops team: Create a quick “How to Use” doc
- Deploy to custom domain: Use
dashboard.yourcompany.cominstead ofyourcompany.retool.com. On Retool Cloud you set this up under Settings → Branding with a CNAME tocustom-domain.retool.com(guide). The pricing page lists branding from the Business plan on
Need help with production monitoring? We set up alerts for slow queries and downtime.
Advanced: Multi-Tab Dashboards
Real internal tools need multiple views:
Add tabs:
- Drag a Tabs component
- Create tabs:
- Orders (your current view)
- Inventory (low stock alerts)
- Customers (support ticket history)
- Analytics (cohort analysis, retention)
Inventory tab example:
Query getLowStockItems:
SELECT
p.id,
p.name,
p.sku,
i.quantity_available,
i.quantity_reserved,
i.reorder_threshold
FROM products p
JOIN inventory i ON p.id = i.product_id
WHERE i.quantity_available < i.reorder_threshold
ORDER BY (i.quantity_available - i.reorder_threshold) ASC;
Display in a table with colour-coded stock levels (red = out of stock, yellow = low, green = good).
Common Pitfalls (And How We Fix Them)
Pitfall 1: Using production DB credentials Always create a read-only user. Write operations get their own user with minimal rights or go through an API layer with validation.
Pitfall 2: N+1 query problems Don’t query inside table row renders. Fetch all data upfront with JOINs.
Pitfall 3: No query result caching
For expensive queries without real-time needs, enable Cache the results of this query under Advanced in the query editor and set the duration in seconds, for example 3600 for one hour (caching). By default the cache is shared across all users of the organization.
Pitfall 4: Hardcoding values Use variables for everything: API keys in Resources, filters in components, not in raw queries.
Pitfall 5: Ignoring mobile users Retool dashboards are desktop-first. For mobile access use Retool Mobile, which builds native iOS and Android apps and is included on every plan according to the pricing page.
Retool vs. Tableau: The Real Comparison
| Feature | Tableau | Retool |
|---|---|---|
| Price per user per month | Standard: Creator $75, Explorer $42, Viewer $15, billed annually | Team: builder €9, internal user €5, billed annually, Free for up to 5 users |
| Data connection | Connectors to data sources | Direct DB queries and REST/GraphQL APIs |
| Customization | Charts, tables, dashboards | Any component, JavaScript, full CRUD |
| Write operations | Not intended | Native (with permission controls) |
| Access per app | Roles Creator, Explorer, Viewer | Levels Use, Edit, Own per group (custom groups from Business) |
| Self-hosting | Tableau Server at the same list prices | On every plan, Enterprise for unlimited users and SSO |
For more details, check our Tableau comparison page.
Need Professional Help?
This dashboard architecture is tested across operations, support, sales and finance teams. Our Custom Dashboard service includes:
- Requirements gathering: Interview your team to understand exact workflow needs
- Multi-source integration: Connect DBs + APIs + spreadsheets in one dashboard
- Custom business logic: Complex calculations, forecasting, anomaly detection
- Access control setup: SSO, role-based permissions, audit logging
- Performance optimization: Query tuning, caching strategies, lazy loading
- Team training: Teach your ops team to edit dashboards themselves
What you save depends on your current licence. Run the numbers with the price lists linked above for your user count, we are happy to help.
Book a free 30-minute dashboard planning session: Schedule here
Related Services
- Analytics Service: Set up data warehouse and ETL pipelines
- Custom Development: Build fully custom web apps when Retool isn’t enough
- Compare vs Retool: When to use Retool vs custom React/Vue apps
Next Guide
Want to connect Retool to Snowflake or BigQuery? Check out our guide on Building Data Warehouse Dashboards with Retool + dbt.