How to Build a Dashboard App: Backend, Database, and UI
A practical guide to building a dashboard app from scratch — covering data models, API design, frontend components, and real-time updates.
What Goes Into a Dashboard App (And Where Most Builds Go Wrong)
Building a dashboard app sounds straightforward until you’re three weeks in, stitching together a half-working frontend, a leaky API, and a database schema you’ve already had to migrate twice. The problem isn’t usually the charts — it’s everything underneath them.
A dashboard app is a full-stack problem. You need a database that models your domain correctly, a backend that surfaces the right data efficiently, a frontend that renders it clearly, and some mechanism for keeping it fresh. Get any layer wrong and the whole thing falls apart.
This guide walks through each layer in order: data modeling, API design, frontend components, and real-time updates. Whether you’re building an internal analytics tool, a SaaS metrics dashboard, or a personal project, the fundamentals are the same.
Step 1: Define What Your Dashboard Actually Needs to Show
Before you write a line of code or create a single table, spend time on this question: what decisions does this dashboard need to support?
Dashboards fail when they’re designed around data that’s available rather than data that’s useful. The result is a wall of numbers that nobody acts on.
Start with the user. Ask:
- Who will use this dashboard?
- What actions will they take based on what they see?
- What’s the time range that matters — today, this week, all-time?
- Does the data change in real time, or is it a daily snapshot?
From those answers, you can derive your metrics, which drives your data model, which shapes your API.
Common dashboard patterns
Most dashboards fit one of a few patterns:
- Operational dashboards — live data, updated frequently, used to monitor running systems (think server health, support queue, delivery tracking)
- Analytical dashboards — historical data, used to spot trends and make decisions (think SaaS revenue, marketing attribution, cohort analysis)
- Entity dashboards — detailed view of a single record (a user profile, an order, a campaign)
The pattern determines your architecture. Operational dashboards need real-time or near-real-time data. Analytical dashboards can often run on pre-aggregated data updated on a schedule. Entity dashboards need efficient lookups with related data joined in.
Step 2: Design Your Data Model
Your data model is the foundation. Get this wrong and everything built on top of it becomes more painful over time.
Think in entities and relationships
Start by listing the core entities your dashboard tracks. For a SaaS product dashboard, that might be:
users— who has an accountsubscriptions— billing and plan infoevents— user activity (page views, feature usage, etc.)revenue— charges, refunds, MRR data
Then define how they relate. A user has one subscription. A user generates many events. Revenue is tied to subscriptions.
Design for your query patterns
A data model that looks clean in a diagram can become a nightmare in practice if it’s not designed around how you’ll query it.
If your dashboard shows “active users in the last 30 days,” you need an events table with an indexed timestamp column. If it shows “MRR by month,” you either need a pre-aggregated revenue_snapshots table or a way to run that aggregation efficiently at query time.
Think ahead about what your most expensive queries will be. Consider:
- Adding indexes on columns you’ll filter or sort by frequently
- Pre-computing expensive aggregations into summary tables
- Keeping raw event data separate from aggregated metrics
For setting up your managed database, options like Supabase or PlanetScale give you managed PostgreSQL with connection pooling built in — both solid choices for dashboard workloads.
A practical schema example
Here’s a simplified schema for a SaaS dashboard:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
plan TEXT DEFAULT 'free'
);
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
event_name TEXT NOT NULL,
properties JSONB,
occurred_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE revenue_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
amount_cents INTEGER NOT NULL,
currency TEXT DEFAULT 'usd',
type TEXT CHECK (type IN ('charge', 'refund')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for common query patterns
CREATE INDEX ON events(user_id, occurred_at DESC);
CREATE INDEX ON revenue_entries(created_at DESC);
This is a starting point, not a final schema. You’ll refine it as you build.
Step 3: Build the Backend API
Your backend has two jobs: read data efficiently and expose it in a shape your frontend can use without extra transformation.
Choose your API style
REST is the default for most dashboard apps. It’s predictable, well-understood, and easy to cache. GraphQL makes sense if your dashboard has highly variable data requirements — different views need very different shapes of data. For most dashboards, REST is the right call.
Structure your endpoints around your dashboard sections
Don’t just CRUD your tables. Design endpoints around what the dashboard actually needs. Instead of /users and /events and /revenue, think:
GET /dashboard/summary— top-level stats (total users, MRR, active users this week)GET /dashboard/charts/revenue?period=30d— revenue over time for the chart componentGET /dashboard/users?status=active&page=1— paginated user tableGET /dashboard/users/:id— entity view for a single user
This approach keeps your frontend simple. It doesn’t need to know how to join or aggregate data — that’s the backend’s job.
Write efficient queries
For a summary endpoint, you might run several queries in parallel:
const [totalUsers, mrr, activeUsers] = await Promise.all([
db.query('SELECT COUNT(*) FROM users'),
db.query(`
SELECT SUM(amount_cents) / 100.0 as mrr
FROM revenue_entries
WHERE type = 'charge'
AND created_at >= date_trunc('month', NOW())
`),
db.query(`
SELECT COUNT(DISTINCT user_id) as count
FROM events
WHERE occurred_at >= NOW() - INTERVAL '7 days'
`)
]);
Running these in parallel with Promise.all keeps the response fast even when pulling from multiple tables.
Add caching where it makes sense
Not every dashboard metric needs to be live. Monthly revenue figures from last quarter don’t change. You can cache those for hours or days. Active session counts might need to be fresh every minute.
Use a simple in-memory cache or Redis for short-lived data. For heavier aggregations, consider a background job that computes them on a schedule and writes results to a cache or summary table.
Step 4: Handle Authentication
A dashboard almost always requires authentication. Users should only see their own data, or you need role-based access to control who sees what.
The decisions here are: who can log in, what can they see, and how do you enforce it?
Set up auth before you build your UI
This sounds obvious but it’s a common mistake to build the dashboard first and bolt auth on later. Auth affects your data model (you need user IDs everywhere), your API (every endpoint needs to verify the requester’s identity), and your frontend routing (unauthenticated users need to be redirected).
For a practical overview of the options and implementation steps, the guide to adding authentication to your web app covers session-based auth, JWT tokens, and third-party auth providers.
Implement row-level security for multi-tenant dashboards
If multiple organizations or users each have their own data, you need to ensure query isolation. PostgreSQL’s row-level security (RLS) lets you enforce this at the database level:
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_events ON events
USING (user_id = current_setting('app.current_user_id')::UUID);
With this in place, a query on events automatically filters to the current user’s data, regardless of what the application layer does.
Supabase has RLS built into its auth system. If you’re comparing backend options, Supabase vs Firebase covers how each handles this.
Step 5: Build the Frontend UI
The frontend of a dashboard has two parts: layout and data visualization. Both are worth thinking about separately.
Layout: sidebar, header, content
Most dashboard layouts follow a similar structure:
- Sidebar — navigation between sections (Overview, Users, Revenue, Settings)
- Header — current page title, date range filters, user account menu
- Content area — the actual metrics, charts, and tables
Keep the sidebar and header as shell components that wrap each page. Route-specific content lives in the content area.
The stat card
The simplest and most common dashboard component is a stat card: a single number with a label and optionally a trend indicator.
type StatCardProps = {
label: string;
value: string | number;
trend?: { value: number; direction: 'up' | 'down' };
};
function StatCard({ label, value, trend }: StatCardProps) {
return (
<div className="p-4 rounded-lg border bg-white">
<p className="text-sm text-gray-500">{label}</p>
<p className="text-2xl font-semibold mt-1">{value}</p>
{trend && (
<p className={trend.direction === 'up' ? 'text-green-600' : 'text-red-600'}>
{trend.direction === 'up' ? '↑' : '↓'} {Math.abs(trend.value)}%
</p>
)}
</div>
);
}
Build a small set of composable components like this and you can construct any dashboard layout quickly.
Charts
For charts, Recharts is a solid React-native option. It’s composable and works well for the common chart types dashboards need: line charts for trends, bar charts for comparisons, pie or donut charts for breakdowns.
A simple revenue chart:
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
function RevenueChart({ data }: { data: { date: string; revenue: number }[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<XAxis dataKey="date" />
<YAxis tickFormatter={(v) => `$${v}`} />
<Tooltip formatter={(v) => `$${v}`} />
<Line type="monotone" dataKey="revenue" stroke="#6366f1" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
);
}
Keep chart components dumb — they receive data as props and render it. Data fetching and transformation happen in a parent component or a custom hook.
Data tables
Tables are often the most complex component in a dashboard. You need sorting, filtering, pagination, and sometimes row actions.
Use a headless table library like TanStack Table (formerly React Table) to handle the logic, and style it yourself. This gives you full control without fighting a rigid component’s styling assumptions.
For pagination, default to cursor-based pagination for large datasets. It’s more efficient than offset pagination and doesn’t break when data is inserted between pages.
Date range filters
Most dashboard sections need a date range control. Build a reusable filter component that manages a startDate / endDate state and broadcasts changes to the page. Include presets (Last 7 days, Last 30 days, This month, Custom) so users don’t have to manually pick dates for common views.
Store the selected range in URL params so dashboard views are shareable and bookmarkable.
Step 6: Add Real-Time Updates
Whether your dashboard needs real-time updates depends on what it’s tracking. An operational dashboard monitoring support tickets or live orders probably does. An analytics dashboard tracking monthly cohorts probably doesn’t.
Polling (the simple approach)
The simplest approach is polling: fetch data on an interval.
useEffect(() => {
const fetch = () => loadDashboardData();
fetch();
const interval = setInterval(fetch, 30_000); // every 30 seconds
return () => clearInterval(interval);
}, []);
Polling works fine for most dashboards. It’s easy to implement, easy to reason about, and handles reconnection automatically. The downside is that it’s not instant — updates arrive up to one interval late.
Server-Sent Events (SSE)
For near-real-time updates without the complexity of WebSockets, Server-Sent Events are a good middle ground. The server pushes updates to the client over a long-lived HTTP connection.
// Server endpoint
app.get('/api/dashboard/live', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
const sendUpdate = () => {
getDashboardSummary().then(data => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
};
sendUpdate();
const interval = setInterval(sendUpdate, 5000);
req.on('close', () => clearInterval(interval));
});
SSE is one-directional (server to client), which is exactly what a dashboard needs. It doesn’t require a separate WebSocket server and works over standard HTTP.
WebSockets (for truly live dashboards)
If you’re building something like a live trading dashboard or a real-time operational monitor, WebSockets give you the lowest latency. Supabase’s Realtime feature wraps this nicely — you can subscribe to database changes without managing WebSocket infrastructure yourself.
Step 7: Deploy It
Once your backend and frontend are working locally, you need to deploy both.
The standard approach for a full-stack dashboard:
- Frontend — deploy to Vercel or Netlify. Both have CI/CD built in: push to main, it deploys.
- Backend — deploy to Railway, Render, or Fly.io. These handle Node/TypeScript backends without the overhead of managing a server yourself.
- Database — use a managed service. Don’t self-host your production database unless you have a specific reason to.
Set up environment variables correctly. Your backend needs database credentials, API keys, and session secrets. Your frontend needs the backend URL. None of this should be committed to git.
For a full walkthrough of the deployment process, the beginner’s guide to deploying a web app covers the options in detail.
How Remy Handles Dashboard App Builds
Building a dashboard app the manual way means making dozens of decisions before you write a single line of product code: which database, which ORM, how to structure the API, how to wire up auth, how to set up deployment. Most of those decisions aren’t the interesting part of your project — they’re just prerequisites.
Remy takes a different approach. Instead of building from code, you describe your application in a spec — a structured document that says what the app does, what the data model looks like, and how the UI should behave. Remy compiles that into a full-stack app: a real TypeScript backend, a SQL database with proper schema, auth, and a deployed frontend.
For a dashboard app, that means you can describe your entities (users, events, revenue), your metrics (active users, MRR, churn), and your UI sections (overview, user table, charts), and get a running application to iterate on rather than starting from a blank editor.
This isn’t a code generator that produces throwaway boilerplate. The spec stays in sync with the codebase as you iterate. When you change the spec, the code updates. It’s closer to working at a higher level of abstraction than to “AI writes code so you don’t have to.”
If you’re building an internal analytics tool, a SaaS metrics dashboard, or any other data-heavy app and want to skip the infrastructure setup, try Remy at mindstudio.ai/remy.
You might also find it useful to build visual dashboards on top of an AI memory system as a reference for more advanced dashboard patterns.
Common Mistakes to Avoid
Even experienced developers make these when building dashboards:
1. Fetching too much data at once Don’t load an entire events table to compute active users. Aggregate on the database side and return a number, not rows.
2. No loading states Dashboards make multiple API calls. Each section should show a skeleton or spinner while it loads — users shouldn’t see a broken layout waiting for slow queries.
3. Not paginating tables A user table with 50,000 rows will break your UI and your backend if you try to load it all. Always paginate.
4. Tight coupling between components and API responses If your chart component directly expects the shape returned by your API, every backend change breaks the frontend. Use a transformation layer — map API responses to a normalized shape before passing to components.
5. Skipping auth entirely for “internal” tools Internal dashboards still get exposed. Either add proper auth or, at minimum, put the app behind a VPN or basic auth. The guide to building internal tools without a dev team covers the practical options here.
Frequently Asked Questions
What tech stack should I use to build a dashboard app?
There’s no single right answer, but a common and well-supported stack is React (with TypeScript) for the frontend, Node.js with Express or Fastify for the backend, and PostgreSQL for the database. For charts, Recharts or Chart.js work well. For the backend, services like Railway or Render make deployment straightforward. If you want to move faster, AI-assisted full-stack builders can handle a lot of the setup automatically.
How do I make my dashboard update in real time?
The simplest option is polling — fetch data on a set interval (every 30–60 seconds). For near-real-time updates, use Server-Sent Events (SSE), which let the server push updates to the browser over a persistent HTTP connection. WebSockets are the lowest-latency option but add complexity. Most dashboards don’t need true WebSocket-level real-time — polling or SSE is usually enough.
How do I handle multi-tenant data in a dashboard?
Each user or organization should only see their own data. Enforce this at the database level with row-level security (RLS) rather than relying solely on application-level filtering. PostgreSQL supports RLS natively. Supabase also builds multi-tenant RLS into its auth system, making it relatively easy to set up.
What’s the best way to display large datasets in a dashboard table?
Use server-side pagination. Fetch one page of results at a time rather than loading everything. Cursor-based pagination (using the last row’s ID or timestamp as a cursor) is more efficient and reliable than offset pagination for large or frequently updated datasets. Libraries like TanStack Table handle the client-side table logic without forcing a specific UI style.
How do I structure my API for a dashboard with multiple sections?
Design your endpoints around dashboard sections, not around raw database tables. A /dashboard/summary endpoint that returns all top-level stats in one request is better than making five separate calls to /users/count, /events/count, etc. This reduces round trips and keeps the frontend simple. Use parallel queries in your backend when a section needs multiple data points.
Should I build a dashboard app from scratch or use an existing tool?
It depends on your requirements. If you need a standard internal analytics view with basic filters and charts, a tool like Retool or Metabase can get you there faster. If you need custom UI, specific business logic, or plan to ship it as a product feature, build it. For SaaS products, building your own dashboard gives you full control over the experience and the data model.
Key Takeaways
- Start with the decisions the dashboard needs to support, not with the data you have available.
- Design your database schema around your query patterns, not just your entities.
- Build backend endpoints around dashboard sections, not around CRUD operations.
- Use polling for most dashboards. Only reach for SSE or WebSockets when you genuinely need real-time updates.
- Handle auth and row-level security before building the UI, not after.
- Keep chart and table components dumb — they receive data as props and render it. Transformation and fetching happen upstream.
If you want to skip the infrastructure setup and start from a working full-stack foundation, try Remy. Describe your dashboard in a spec, and it compiles into a real app — backend, database, auth, and frontend included.