The Internal Dashboard Trap
Building your own internal dashboard feels like the smart move - until someone asks for login, audit logs, and a mobile view. I've learned this lesson twice.
Every internal dashboard starts the same way. Someone needs to see order totals, or patient counts, or inventory levels. You build a quick Laravel route, throw some Blade on it, wire up a Chart.js bar chart, and ship it in an afternoon. It works great. Then three months later you're staring down a Jira ticket that says "add login so the warehouse team can't see the billing data" and you realize the afternoon project just became a two-week project.
I've built internal dashboards from scratch at least a dozen times across my clients. I've also watched the scope creep swallow those projects whole. This is my honest accounting of where the line is.
What You're Actually Building
The pitch for rolling your own is always the same: "It's just a few queries and some tables. We don't need a whole platform for this." That's true on day one. But internal tools have a way of attracting requirements the way a ship attracts barnacles.
Here's what "a few queries and some tables" actually becomes over 6-12 months, based on real projects:
- Authentication with role-based access (not everyone should see revenue)
- Audit logs (who changed that order status, and when)
- Mobile layout (the warehouse manager is on a tablet)
- Export to CSV/Excel (always)
- Pagination on the big tables (turns out 80,000 rows is slow)
- Search and filter on every column someone cares about
- Email alerts when a metric crosses a threshold
- Multi-environment support (staging vs production data)
None of these are hard individually. Together they're a small product. And if you're a consulting shop like mine, you're billing that time to a client who was expecting "a few queries and some tables."
The Auth Problem Is Nastier Than It Looks
I hit this hard on a project for a Seattle-area e-commerce client a couple of years ago. The dashboard was beautiful, fast, and completely open to anyone with the URL. When they hired their third warehouse employee and needed to hide the margin data, I had to retrofit auth into a dashboard I'd already wired directly to their production database credentials stored in .env.
Adding Laravel Sanctum or Breeze to an existing app isn't the hard part. The hard part is the role model. Who can see what? Here's the kind of table you end up needing:
// Migration for the role-permission pivot you didn't plan for
Schema::create('dashboard_permissions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('section'); // 'revenue', 'inventory', 'fulfillment'
$table->boolean('can_view')->default(false);
$table->boolean('can_export')->default(false);
$table->timestamps();
});
Now you need a UI to manage that table. And someone to maintain it when the org chart changes. And documentation so the client's office manager knows how to add a new user without calling you.
That's not a dashboard anymore. That's an admin panel for your admin panel.
Audit Logs Are Non-Negotiable in Regulated Industries
I work with healthcare and biotech clients. In those spaces, "who viewed this record and when" is not a nice-to-have. It's a compliance requirement. When I first started rolling custom dashboards for a biotech client with LIMS data, I didn't build audit logging in from the start. That was a mistake that cost us a sprint to fix.
Audit logging sounds easy:
// You think this is all you need
ActivityLog::create([
'user_id' => auth()->id(),
'action' => 'viewed_patient_summary',
'record_id' => $patientId,
'ip_address' => request()->ip(),
'created_at' => now(),
]);
But then you need to log exports separately. And failed login attempts. And filter changes that expose different data subsets. And you need a UI to query those logs. And retention policies. And the ability to export the logs themselves for an audit.
I've started using the spatie/laravel-activitylog package for this because it handles model-level logging reasonably well. But wiring it up to a dashboard where the "model" is a raw SQL query against a reporting database is always a custom job.
The Mobile Layout Problem Is the One That Breaks Morale
The warehouse manager is on a tablet. The field rep checks numbers on her phone between client visits. The ops director looks at the dashboard from his phone on weekends. You will hear this request. It always comes after launch.
Responsive tables are a special kind of painful. Tailwind helps, but a data-dense table with 12 columns does not gracefully collapse to 375px without real design work. You either need to completely rethink the layout for mobile or accept a horizontal scroll that nobody likes.
This is usually the moment I start asking whether we should have bought something instead of built it.
When I'd Reach for Retool (or a Competitor)
Retool, Appsmith, Budibase, and similar tools exist exactly for this gap. They handle auth (including SSO), have audit logs built in, and their table components are responsive by default. You connect them to your database or API and you're mostly writing queries instead of building infrastructure.
Here's what a Retool REST API resource query looks like when I wire it to a Laravel API endpoint:
GET https://api.example.com/v1/dashboard/orders
Headers:
Authorization: Bearer {{current_user.authToken}}
X-Dashboard-Context: retool
Query Params:
start_date: {{dateRangePicker.startDate}}
end_date: {{dateRangePicker.endDate}}
status: {{statusFilter.value}}
And the Laravel side:
// routes/api.php
Route::middleware(['auth:sanctum', 'role:dashboard'])
->get('/dashboard/orders', [DashboardController::class, 'orders']);
// DashboardController.php
public function orders(Request $request): JsonResponse
{
$validated = $request->validate([
'start_date' => 'required|date',
'end_date' => 'required|date|after_or_equal:start_date',
'status' => 'nullable|in:pending,fulfilled,cancelled',
]);
$query = Order::query()
->whereBetween('created_at', [$validated['start_date'], $validated['end_date']])
->when($validated['status'] ?? null, fn ($q, $s) => $q->where('status', $s))
->select('id', 'status', 'total', 'created_at', 'customer_id')
->with('customer:id,name,email');
return response()->json([
'data' => $query->paginate(50),
'meta' => [
'total_revenue' => (clone $query)->sum('total'),
'order_count' => (clone $query)->count(),
],
]);
}
This way I build the data layer in Laravel where I'm comfortable and where the business logic belongs. Retool handles the presentation layer. Auth is managed in Retool (with SSO to Google or whatever the client uses). Audit logs are on by default. Mobile layout is handled by Retool's grid system, which is genuinely pretty good for table-heavy interfaces.
The tradeoff is cost ($10-$50/user/month depending on plan) and the fact that Retool is hosted SaaS, which some clients can't accept for compliance reasons. Appsmith has a self-hosted open source option that I've used for healthcare clients where data can't leave their infrastructure.
When I'd Still Build It From Scratch
If the dashboard is public-facing, scratch it. Retool and its cousins are for internal tools with controlled user counts. If you're building something your customers log into, the per-seat pricing gets ugly fast and the UI won't match your brand.
If the data logic is extremely complex, scratch it. I had a client with a reporting requirement that needed about 15 joins across three databases. Expressing that in a Retool query editor was not fun. Sometimes the complexity is better handled in a proper service layer where you can write tests.
If the user count is under 3 and the scope is genuinely stable, scratch it. A simple read-only reporting page for one internal user who just needs to see a number is not worth the overhead of standing up Retool. Build the Blade view, put it behind basic auth, and move on.
The honest rule I've landed on: if the dashboard needs more than one user role, or if it needs to write data back (approve, reject, update records), the cost of building auth and audit logging from scratch almost never beats the cost of a low-code platform. The platform wins on time, even accounting for its constraints.
The Decision Framework I Actually Use
Before I start building, I ask the client four questions:
- How many different types of users will access this?
- Does anyone need to take action (not just view) from the dashboard?
- Are you in a regulated industry where access logs matter?
- Do you expect to access this from a phone or tablet?
If the answer to any two of those is yes, I'm pitching a low-code platform and building a solid API behind it. If the answer to all four is no, I'll build a dead-simple Blade dashboard and get on with my life.
The trap is not building it yourself. The trap is starting to build it yourself and then having requirements change under you. That's when you end up with 2,000 lines of dashboard scaffolding that you now have to maintain forever.
Building software is fine. Building the wrong software because you didn't ask the right questions upfront is expensive. I've made that mistake enough times that I've stopped making it.
Need help shipping something like this? Get in touch.