Tulip Logo IconTulip
Quick Start

Build your first admin screen

Add a protected admin route, sidebar item, and first content page to a Tulip app.

This page assumes you are using a generated Tulip app. The basic template already contains the root providers, auth setup, and admin layout, so the first useful step is adding your own admin route.

1. Check the root layout

The root layout should wrap the app in Tulip providers:

import "./globals.css";
import "../server/router/register.js";

import { Providers } from "@tulip-systems/app/components/client";
import { generateRootLayoutMetadata, RootLayout } from "@tulip-systems/app/components/server";
import type { PropsWithChildren } from "react";
import { config } from "@/lib/config/base";

export const { metadata, viewport } = generateRootLayoutMetadata(config);

export default function Layout({ children }: PropsWithChildren) {
  return (
    <RootLayout>
      <Providers>{children}</Providers>
    </RootLayout>
  );
}

2. Check the admin layout

Your admin route should use the protected Tulip admin shell:

import { AdminContent } from "@tulip-systems/app/components/client";
import { AdminLayout } from "@tulip-systems/app/components/server";
import { AuthProvider } from "@tulip-systems/auth/client";
import { AuthGuard } from "@tulip-systems/auth/next/guards";
import type { PropsWithChildren } from "react";
import { AdminSidebar } from "@/lib/config/paths";
import { authClient } from "@/server/auth/client";
import { auth } from "@/server/auth/init";

export default async function Layout(props: PropsWithChildren) {
  return (
    <AuthGuard auth={auth}>
      <AuthProvider authClient={authClient}>
        <AdminLayout>
          <AdminSidebar />
          <AdminContent>{props.children}</AdminContent>
        </AdminLayout>
      </AuthProvider>
    </AuthGuard>
  );
}

The generated app already includes this pattern. Keep it in src/app/admin/layout.tsx so every admin page shares the same protection and shell.

3. Add a page

Create src/app/admin/customers/page.tsx:

import { Card, CardContent, CardHeader, CardTitle } from "@tulip-systems/ui/card";

const customers = [
  { name: "Acme Studio", status: "Active" },
  { name: "Northwind", status: "Onboarding" },
  { name: "Bluebird Logistics", status: "Paused" },
];

export default function Page() {
  return (
    <div className="grid gap-5 p-content">
      <Card>
        <CardHeader>
          <CardTitle>Customers</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="divide-y rounded-md border">
            {customers.map((customer) => (
              <div className="flex items-center justify-between p-4" key={customer.name}>
                <span className="font-medium">{customer.name}</span>
                <span className="text-muted-foreground text-sm">{customer.status}</span>
              </div>
            ))}
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

This is deliberately static. The goal is to confirm the shell, route, spacing, and UI imports before adding server data.

4. Add a sidebar item

Add the route to your admin sidebar, usually in src/lib/config/paths.tsx:

import { LayoutDashboardIcon, UsersIcon } from "lucide-react";
import {
  Path,
  PathGroup,
  PathGroupItems,
  PathLink,
  Sidebar,
  SidebarContent,
  SidebarHeader,
  SidebarRail,
} from "@tulip-systems/app/components/client";

export function AdminSidebar() {
  return (
    <Sidebar>
      <SidebarHeader />
      <SidebarContent>
        <PathGroup>
          <PathGroupItems>
            <Path segment="(dashboard)">
              <PathLink href="/admin">
                <LayoutDashboardIcon />
                Dashboard
              </PathLink>
            </Path>
            <Path segment="customers">
              <PathLink href="/admin/customers">
                <UsersIcon />
                Customers
              </PathLink>
            </Path>
          </PathGroupItems>
        </PathGroup>
      </SidebarContent>
      <SidebarRail />
    </Sidebar>
  );
}

If your generated app already has account menu or footer content, keep it. Only add the new customers path.

5. Run the app

Start the dev server and open the new page:

pnpm dev

Visit /admin/customers after signing in.

What to add next

Once the static page works, replace the local array with real data and package-specific behavior:

  • Use Query for filters, sorting, pagination, and URL-backed state.
  • Use Data Tables when the list needs column configuration and loading strategies.
  • Use Commands when rows need actions such as archive, restore, delete, or status updates.
  • Use Inline when users should edit fields without opening a full form.

Next step

On this page