Appsmith: Internal Tool Builder That Actually Handles Complex Business Logic
The ops team was running customer support on spreadsheets. 50 support agents, 200 tickets/day, tracked in a shared Google Sheet with color-coded cells. It was a disaster. They needed a proper ticket management system. The IT team quoted 4 months. I built it in Appsmith in 3 days. Self-hosted, connected to their existing PostgreSQL database, with role-based views and automated escalations. Here's how.
Why Appsmith Over Retool for Self-Hosting
I compared Appsmith and Retool for this project. Key differences:
| Feature | Retool | Appsmith |
|---|---|---|
| Self-hosted | Enterprise plan only ($$$) | Free (open-source) |
| Pricing model | Per user/month | Free self-hosted |
| Git version control | Available | Available |
| JS in queries | Full JS support | Full JS support |
| Custom components | React components | React + HTML widgets |
| Community | Smaller | Larger (GitHub stars) |
The deal-breaker was cost. 50 support agents on Retool would cost thousands per month. Appsmith self-hosted is free. The features are comparable. Appsmith wins for self-hosted use cases.
Connecting to Multiple Data Sources
The ticket system needed data from three places:
- PostgreSQL — main ticket and customer data
- REST API — external CRM for customer details
- Redis — real-time agent status (online/busy/away)
// In Appsmith, data sources are configured in the UI
// Each query runs against a specific data source
// Query: get_tickets (PostgreSQL)
// SELECT * FROM tickets
// WHERE assigned_to = {{appsmith.user.email}}
// AND status IN ({{JSON.stringify(Table1.selectedRows.map(r => r.status))}})
// ORDER BY priority DESC, created_at ASC
// LIMIT 50
// Query: get_customer_from_crm (REST API)
// GET https://crm.example.com/api/customers/{{ticket.customer_id}}
// Headers: Authorization: Bearer {{appsmith.store.crmToken}}
// Query: get_agent_status (Redis)
// GET agent:status:{{agent_email}}
// These queries can be chained in JS
Writing Complex JS Queries
This is where Appsmith separates itself from simpler low-code tools. You write real JavaScript for business logic:
// JS Object: ticketActions
// This runs server-side in Appsmith's sandbox
export default {
// Escalate a ticket: update status, reassign, notify manager
async escalateTicket(ticketId, reason) {
// Step 1: Get current ticket
const ticket = await get_ticket.run({ id: ticketId })
// Step 2: Find the escalation manager based on department
const manager = await find_manager.run({
department: ticket.department,
level: "escalation",
})
// Step 3: Update the ticket
await update_ticket.run({
id: ticketId,
status: "escalated",
assigned_to: manager.email,
escalation_reason: reason,
escalated_at: new Date().toISOString(),
})
// Step 4: Log the escalation
await create_audit_log.run({
action: "ticket_escalated",
ticket_id: ticketId,
from_agent: ticket.assigned_to,
to_agent: manager.email,
reason: reason,
timestamp: new Date().toISOString(),
})
// Step 5: Send notification via Novu
await fetch("https://api.novu.co/v1/events/trigger", {
method: "POST",
headers: {
"Authorization": `ApiKey ${appsmith.store.novuKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "ticket-escalated",
to: { subscriberId: manager.email, email: manager.email },
payload: {
ticketId,
customerName: ticket.customer_name,
reason,
fromAgent: ticket.assigned_to,
},
}),
})
// Step 6: Refresh the ticket list
await get_tickets.run()
// Show success message
showAlert("Ticket escalated to " + manager.email, "success")
},
// Bulk close resolved tickets older than 7 days
async bulkCloseOldTickets() {
const sevenDaysAgo = new Date()
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
const tickets = await get_resolved_tickets.run({
before_date: sevenDaysAgo.toISOString(),
})
let closed = 0
for (const ticket of tickets) {
await close_ticket.run({ id: ticket.id })
closed++
}
showAlert(`Closed ${closed} tickets`, "success")
await get_tickets.run()
},
}
Problem
The bulk close function ran sequentially — each ticket took 200ms for the API call. With 500 tickets, it took 100 seconds. Appsmith's default timeout is 30 seconds. The function would fail halfway through.
What I Tried
Tried increasing the timeout in Appsmith settings. That helped but didn't scale. Tried batching the SQL updates into one query — better, but some tickets needed individual API calls.
Actual Fix
Use Promise.all for parallel execution and batch SQL updates:
// Parallel execution with controlled concurrency
async bulkCloseOldTickets() {
const tickets = await get_resolved_tickets.run({})
// Batch the SQL updates — one query instead of 500
const ids = tickets.map(t => t.id)
await bulk_close_tickets.run({ ids }) // UPDATE tickets SET status='closed' WHERE id = ANY({{ids}})
// For individual API calls that can't be batched:
// Use Promise.all with a concurrency limit
const BATCH_SIZE = 10
for (let i = 0; i < tickets.length; i += BATCH_SIZE) {
const batch = tickets.slice(i, i + BATCH_SIZE)
await Promise.all(
batch.map(ticket => send_close_notification.run({ ticketId: ticket.id }))
)
}
showAlert(`Closed ${tickets.length} tickets`, "success")
}
// In the PostgreSQL query: bulk_close_tickets
// UPDATE tickets
// SET status = 'closed', closed_at = NOW()
// WHERE id = ANY({{ids}}::int[])
Git Version Control for Apps
This is critical for team development. Appsmith's Git sync lets you branch, commit, and review changes like code:
# Connect Appsmith to Git
# 1. In Appsmith settings, go to "Git Sync"
# 2. Connect to your Git repository
# 3. Appsmith commits the app JSON to the repo
# The repo structure looks like:
# ├── ticket_system/
# │ ├── metadata.json # App metadata
# │ ├── datasources/ # Database connections
# │ ├── pages/
# │ │ ├── Dashboard/
# │ │ │ ├── canvas.json # UI layout
# │ │ │ └── queries/ # Page-specific queries
# │ │ └── TicketDetail/
# │ └── js-objects/ # Shared JS functions
# Workflow:
# 1. Developer creates a branch in Appsmith
# 2. Makes changes to the app
# 3. Commits and pushes to Git
# 4. PR review in GitHub/GitLab
# 5. Merge triggers deployment to production
# This means:
# - Multiple developers can work on the same app
# - Changes are reviewed before going live
# - You can roll back to any previous version
# - Audit trail of who changed what and when
Problem
Two developers edited the same page at the same time. Both pushed to Git. The JSON merge conflict left the app in a broken state — widgets had missing properties and some queries disappeared.
Actual Fix
Avoid merge conflicts by splitting work across pages or JS objects. If conflicts happen, resolve in Git and re-import:
# If merge conflict happens:
# 1. Don't try to fix it in Appsmith UI
# 2. Fix the JSON conflict in your Git tool
# 3. Accept one version of the conflicting file
# 4. Push the resolution
# 5. In Appsmith, pull the resolved version
# Prevention: assign pages to developers
# Developer A: Dashboard page + dashboard queries
# Developer B: TicketDetail page + ticket queries
# Developer C: JS objects + shared functions
Deploying to Production
# Self-hosted deployment with Docker
docker run -d \
--name appsmith \
-p 8080:80 \
-v appsmith_stacks:/appsmith-stacks \
appsmith/appsmith-ee:latest
# For production, use Docker Compose with:
# - PostgreSQL for Appsmith internal DB
# - MongoDB for Appsmith metadata
# - Redis for session management
# - Nginx for SSL termination
# docker-compose.yml highlights:
version: "3"
services:
appsmith:
image: appsmith/appsmith-ee:latest
ports: ["8080:80"]
volumes: ["./stacks:/appsmith-stacks"]
environment:
- APPSMITH_MAIL_ENABLED=true
- APPSMITH_MAIL_HOST=smtp.example.com
- APPSMITH_MAIL_USERNAME=notifications@example.com
- APPSMITH_MAIL_PASSWORD=****
- APPSMITH_DISABLE_TELEMETRY=true
- APPSMITH_GOOGLE_MAPS_API_KEY=****
What I Learned
- Appsmith handles complex logic through JS objects. This isn't just drag-and-drop — you write real JavaScript for business rules.
- Batch database operations. Don't loop through rows making individual queries. Use batch SQL and
Promise.allfor API calls. - Git sync is essential for team development. Without it, only one person can edit the app at a time.
- Split work across pages to avoid conflicts. Each page has its own JSON file. Different developers on different pages = no conflicts.
- Self-hosted Appsmith is truly free. No user limits, no feature gates. You just need a server and someone to maintain it.
- The widget library is good but not infinite. For custom UI needs, use the Custom Widget (React component embedding).
Wrapping Up
Appsmith replaced a spreadsheet-based support system for 50 agents. The ticket management app handles 200+ tickets/day, connects to three different data sources, and includes automated escalation logic. The JS query system handles complex business rules that simpler low-code tools can't. And the self-hosted deployment costs nothing beyond the server. For enterprise internal tools where cost and control matter, Appsmith is the right choice.