Skip to content
Every tutorial here is 100% free — and always will be
LSLovableStudent

make admin dashboard

How to make an admin dashboard

An admin dashboard is three things stacked together: a role check that lives in the database, a guarded route group only admins can open, and read-only views of your real data. Build them in that order. If you build the screens first and add the role check later, every admin page you shipped in between is public.

What an admin dashboard actually is

Most people picture the charts. The charts are the easy part. A dashboard that survives contact with real users is four layers:

┌──────────────────────────────────────────────────────────┐
│ 4. Presentation   cards, tables, charts, filters, export │
├──────────────────────────────────────────────────────────┤
│ 3. Data layer     server-side queries + aggregates       │
├──────────────────────────────────────────────────────────┤
│ 2. Access control guarded /admin routes + redirects      │
├──────────────────────────────────────────────────────────┤
│ 1. Identity       who is signed in, and are they admin?  │
└──────────────────────────────────────────────────────────┘

Build bottom-up. Layer 1 and 2 take an afternoon and are the only layers that can hurt you.

Step 1 — Store roles in their own table

The single most common mistake: a role or is_admin column on the user's own profile row. Users can usually edit their own profile, so that design lets anyone promote themselves to admin. Roles belong in a separate table that users cannot write to.

users ──1:many──▶ user_roles
                  ├── user_id
                  └── role  (admin | editor | user)

              ▲
              │ read-only to the user
              │ writable only by an existing admin

Prompt to use:

Create a separate user_roles table with user_id and a role enum of admin, editor and user. Users may read their own roles but never insert, update or delete them. Add a security-definer function has_role(user_id, role) and use it in the row-level security policies for admin-only data. Do not put a role column on the profiles table.

Then make yourself the first admin — there is no chicken-and-egg trick, someone has to be inserted directly:

Insert an admin role row for my account, matched by my email address.

Step 2 — Guard the route group, not the buttons

Hiding the "Admin" link is not security. The page still answers if someone types the URL, and the data still returns if someone calls the endpoint. You need three checks in the same place:

request /admin/orders
        │
        ▼
  signed in? ── no ──▶ redirect /auth
        │ yes
        ▼
  has admin role? ── no ──▶ redirect / with "not authorised"
        │ yes
        ▼
  render dashboard  ──▶ every query re-checks the role server-side

The third arrow is the one people skip. A guard on the page stops the page rendering; it does not stop a hand-written request to your data. Every admin query must verify the role again on the server.

Prompt to use:

Put all admin pages under a guarded /admin route group. Unauthenticated visitors are redirected to /auth; signed-in users without the admin role are redirected home with a clear message. Show a loading state while the role is being checked, never the dashboard. Every admin data query must also verify the admin role server-side.

Step 3 — Design the information layers

Do not put everything on one screen. A dashboard that works has three depths:

Depth Purpose Example
Overview Is anything wrong right now? Signups today, revenue this week, failed payments, error count
List Find one specific record Searchable, filterable, paginated table of orders or users
Detail Act on that record One order: full history, refund button, internal notes
/admin                /admin/orders            /admin/orders/1042
┌──────────────┐      ┌──────────────────┐     ┌──────────────────┐
│ ▣ ▣ ▣ ▣  KPIs│ ───▶ │ search · filters │ ──▶ │ record detail    │
│ ╱╲ trend     │      │ ─────────────────│     │ timeline         │
│ recent items │      │ row  row  row    │     │ actions          │
└──────────────┘      │ ◀ 1 2 3 ▶        │     └──────────────────┘
└──────────────┘

Pick four to six numbers for the overview. More than that and nobody reads any of them.

Step 4 — Build the data layer server-side

Never load every row into the browser and count in JavaScript. It is slow at a thousand rows and broken at a hundred thousand, and it hands the browser data the user should not see.

BAD                              GOOD
select * from orders             select count(*), sum(total)
  → filter in React               → group by day, in the database
  → 40,000 rows over the wire     → 30 rows over the wire

Prompt to use:

Create server functions for the admin metrics: total users, signups in the last 7 days, revenue this month, and a daily count for the last 30 days. Aggregate inside the database, return only the computed numbers, and verify the caller has the admin role first. Cache the result for 60 seconds.

For tables, ask for server-side pagination, sorting and search — 25 rows per request, not the whole table.

Step 5 — Layout, charts and the boring details that make it usable

  • Sidebar navigation with the sections grouped; a dashboard grows and a top nav runs out of room.
  • Sensible empty states. "No orders yet" beats a blank rectangle.
  • Skeletons, not spinners, so the layout does not jump.
  • Timestamps in the viewer's timezone, with relative labels ("3 hours ago") on recent items.
  • Charts only where a trend matters. A single number does not need a chart; a 30-day count does.
  • Destructive actions behind a confirm dialog, and never a delete button in a table row next to "view".
  • Mobile fallback. Admins check things from a phone. Tables should collapse to cards.

Prompt to use:

Build the /admin layout with a collapsible sidebar, a page header with breadcrumbs, and a content area. Use skeleton loaders, empty states for every list, relative timestamps, and a confirm dialog for destructive actions. Tables collapse to stacked cards on mobile.

Step 6 — Audit what admins do

The moment more than one person has admin access, you need a record of who changed what. It is cheap to add now and impossible to reconstruct later.

Add an admin_audit_log table recording actor user_id, action, target table, target id, a JSON diff and a timestamp. Write to it from every admin mutation, and add an /admin/audit page that lists it newest-first with filters by actor and action.

Full build order (copy this)

  1. Roles table + has_role function + policies.
  2. Insert yourself as admin.
  3. Guarded /admin route group with redirects.
  4. Overview page with four real metrics from server-side aggregates.
  5. One list page with server-side search and pagination.
  6. One detail page with the actions you actually need.
  7. Audit log.
  8. Extra sections, one at a time.

Mistakes that break admin dashboards

  • is_admin on the profile row. Self-promotion in one request.
  • Hidden links treated as security. The URL still works.
  • Client-side role checks only. The data endpoint has no idea who is asking.
  • Counting in the browser. Falls over the day you succeed.
  • No loading state on the role check, so the dashboard flashes for a second before the redirect — long enough to read.
  • One giant page. Every new section makes it slower for everyone.
  • Admin and public data in the same unguarded query, so one endpoint leaks everything.

How to check it is actually safe

Test with a second, non-admin account — not by logging out:

  1. Sign in as a normal user and type /admin directly. You must be redirected.
  2. Sign in as a normal user and call an admin endpoint from the browser console. It must refuse, not return data.
  3. As a normal user, try to insert an admin row for yourself into the roles table. It must fail.
  4. Sign out entirely and repeat step 1.

If all four behave, the dashboard is sound. If any returns data, stop building screens and fix the layer underneath.

Frequently asked questions

How do I make an admin dashboard?
Store roles in a separate user_roles table, guard an /admin route group so only users with the admin role can open it, then add pages that read server-side aggregates. Build the role check first — screens built before it are public.
Where should admin roles be stored?
In their own table that users can read but never write, checked by a security-definer function used in your row-level security policies. A role column on the user's own profile row lets anyone make themselves an admin.
Is hiding the admin link enough to protect admin pages?
No. The URL still works and the data endpoints still answer. You need a route guard plus a server-side role check inside every admin query.
How do I become the first admin?
Insert an admin role row for your own account directly, matched by your email. There is no way around it — someone has to be promoted outside the app.
Should the dashboard calculate its numbers in the browser?
No. Aggregate in the database and return only the computed values. Loading full tables into the browser is slow and exposes data the viewer should not have.

Go deeper

Related guides

Website made, maintained and hosted by PromptSites.com

PromptSites logo