March 20, 2025
Patterns for building a secure, well-structured admin panel — route protection, data tables, and role-based access with adminProcedure.

The admin panel lives under app/admin/ and is protected at the middleware level. Each resource gets its own folder with a page.tsx and a columns.tsx:
app/admin/
layout.tsx ← sidebar + auth guard
page.tsx ← overview/dashboard
users/
page.tsx
columns.tsx
products/
page.tsx
columns.tsx
orders/
page.tsx
columns.tsx
Guard the entire admin tree in middleware:
// middleware.ts
if (request.nextUrl.pathname.startsWith("/admin")) {
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user || session.user.role !== "admin") {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
}All admin data goes through tRPC using adminProcedure, which checks the session role server-side before executing:
// services/trpc/init.ts
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});// services/trpc/routers/admin/users.ts
export const usersRouter = router({
list: adminProcedure.query(() => db.query.users.findMany()),
delete: adminProcedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) =>
db.delete(users).where(eq(users.id, input.id))
),
});Always use the shared <DataTable> component. Columns go in columns.tsx:
// app/admin/users/columns.tsx
import { ColumnDef } from "@tanstack/react-table";
export const columns: ColumnDef<User>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
{
id: "actions",
cell: ({ row }) => <UserActions user={row.original} />,
},
];// app/admin/users/page.tsx
const UsersPage = () => {
const trpc = useTRPC();
const { data, isPending } = useQuery(trpc.admin.users.list.queryOptions());
if (isPending) return <Skeleton className="h-96 w-full" />;
return <DataTable columns={columns} data={data ?? []} />;
};adminProcedureAlertDialog for confirmationBadge for status labels, DropdownMenu for row actionsYour laboratory instruments should serve you, not the other way around. We're happy to help you.