Every SaaS founder eventually has to look something up manually. A customer emails saying they cannot log in. A subscription did not activate after payment. Someone requests account deletion under GDPR. You open your database client, run a query, and fix it by hand.
That works for the first ten customers. It does not scale past the first hundred.
Most founders reach for an external admin tool at this point -- Retool, Appsmith, or a hosted dashboard service. That feels fast at first, but two problems show up later: the external tool has its own access model that drifts out of sync with your actual permissions, and it has no audit trail unless you wire one up yourself.
If your Next.js app already has auth, a database, and an API layer, you have everything you need to build a scoped admin panel inside your own product.
You do not need a chart-heavy BI tool. A basic SaaS admin panel has four jobs:
A table, a search input, a few action buttons, and an audit trail. That is the whole thing.
External tools feel faster to set up initially. You connect a database, drag a table component, and you are done. But two things start costing you later.
First, external tools have their own access model. Anyone with tool access can query any table. Scoping it down to "support can see users but not payment details" requires separate configuration that drifts out of sync with your actual app permissions.
Second, you lose the audit trail. When a support agent suspends an account in Retool, there is no record in your own database unless you wire it manually. Inside your codebase, every admin action writes to the same audit log your product already uses.
An internal admin panel also adds no new service to secure and pay for. You are adding a protected route group to an app you already deploy and own.
Create a route group at app/(admin)/ with its own layout. The layout checks that the authenticated user has role === 'admin' and redirects everyone else. This is the same role check described in role-based access control applied at the layout level instead of inside individual service functions.
Inside the group, one page does most of the work: app/(admin)/users/page.tsx. It is a server component that reads ?q= and ?page= from the URL and runs a Drizzle query against your users table:
// simplified -- full filter logic lives in userRepo
const users = await userRepo.searchByEmail(q, { page, limit: 25 })
No client state, no separate API endpoint needed. The result renders as a shadcn Table with status badges and action buttons. Each action -- suspend, activate, send a reset link -- posts to a thin API route that calls the relevant service function and writes an audit log entry with the admin user ID, target user ID, action name, and timestamp.
Start with the ones that come up every week:
suspended flag on the user row; your JWT validation step already rejects suspended users if you add that check to getUserFromRequest()Every action writes an audit log row. Six months from now, when a user disputes a suspension, you have a record with the admin's ID, the action, and the exact timestamp.
An internal admin panel makes sense when you want support staff to take specific actions without giving them raw database access, when you need a paper trail tied to your existing user IDs, and when you want permissions enforced by the same role system your product already uses.
It does not make sense if you have complex reporting needs across many tables -- a BI tool like Metabase is the right answer for that.
If you are starting from this Next.js SaaS boilerplate, the user table, JWT auth, service layer, and role column are already in place. You are adding a route group and a search page -- not building a data platform from scratch.
Build the admin panel inside your product, not alongside it. Get the boilerplate and ship the first admin view this weekend.