Skip to content
Tikab's Toolkit

The admin area

/admin is the fullest composition example in the repo: the admin-ui blocks assembled against the app's site-admin-gated server functions. Sign in as admin@example.com / password123 and type the URL — the area is deliberately unlinked and 404s for non-admins.

The whole area is ~50 lines of composition

The route file: nav items into the shell, the gate in beforeLoad, sections as child routes, the app's account controls in the shell's accountSlot. Live code:

import { createFileRoute, notFound, Outlet } from "@tanstack/react-router";
import {
  Building2,
  FileStack,
  FlaskConical,
  LayoutDashboard,
  Mail,
  ScrollText,
  Settings2,
  Shield,
  Users,
} from "lucide-react";
 
import { AdminShell, type AdminNavItem } from "@repo/admin-ui";
import { AccountControls } from "#/components/AccountMenu";
import { NotFoundScreen } from "#/components/system/SystemScreens";
import { m } from "#/paraglide/messages";
 
/**
 * Site administration — the jazzmin/unfold replacement, on-prem by
 * construction (runs inside the app: no CDN, no cloud console, no license
 * activation). Its own chrome (AdminShell: left nav, mobile drawer) — admin
 * is deliberately not dressed like the app. The shell is string-free by
 * contract; every label comes from Paraglide here.
 *
 * Gated on `isSiteAdmin` both here (404 — don't reveal the area exists) and
 * in every server function (`requireSiteAdmin`). Project-scoped admin (roles,
 * templates) stays inside each project's AdminPanel.
 */
export const Route = createFileRoute("/_authed/admin")({
  beforeLoad: ({ context }) => {
    const user = (context as { user?: { isSiteAdmin?: boolean } }).user;
    if (!user?.isSiteAdmin) throw notFound();
  },
  notFoundComponent: () => <NotFoundScreen />,
  component: AdminLayout,
});
 
function AdminLayout() {
  const nav: AdminNavItem[] = [
    { to: "/admin", label: m.sadmin_nav_overview(), icon: LayoutDashboard, exact: true },
    { to: "/admin/users", label: m.sadmin_nav_users(), icon: Users },
    { to: "/admin/groups", label: m.sadmin_nav_groups(), icon: Shield },
    { to: "/admin/workspaces", label: m.sadmin_nav_workspaces(), icon: Building2 },
    { to: "/admin/media", label: m.sadmin_nav_media(), icon: FileStack },
    { to: "/admin/config", label: m.sadmin_nav_config(), icon: Settings2 },
    { to: "/admin/audit", label: m.sadmin_nav_audit(), icon: ScrollText },
    { to: "/admin/outbox", label: m.sadmin_nav_outbox(), icon: Mail },
    // Lives outside /admin but shares the gate: site admins only in prod.
    { to: "/sandbox", label: m.sadmin_nav_sandbox(), icon: FlaskConical },
  ];
 
  return (
    <AdminShell
      items={nav}
      title={m.sadmin_title()}
      subtitle={m.sadmin_subtitle()}
      back={{ to: "/", label: m.sadmin_back() }}
      menuLabel={m.sadmin_menu_open()}
      closeMenuLabel={m.sadmin_menu_close()}
      collapseLabel={m.sadmin_nav_collapse()}
      expandLabel={m.sadmin_nav_expand()}
      accountSlot={<AccountControls withSignOut compact />}
    >
      <Outlet />
    </AdminShell>
  );
}

Gating happens twice, on purpose: here (404 — the area's existence is not revealed) and in every server function (requireSiteAdmin()); the UI gate is convenience, the server gate is security.

The sections

SectionDoesDjango parallel
Userssearch, select-many → edit simultaneously (status/site-admin/name), bulk activate/deactivate, password resets, two-phase CSV import (preview → confirm), selection-aware exportUser admin + import-export + actions
Groupsglobal capability groups + membershipGroups
Workspacessite-wide overview + creation
Mediaevery attachment, filename search + status filter, queue AI re-indexing for a selectionfile management + admin actions
Configruntime keys without a deployconstance admin
Auditthe global change historyLogEntry, but real
Outboxevery recorded/sent email

Behaviors worth knowing

  • Deactivation takes effect immediately — the session choke point treats deactivated users as anonymous, so even live sessions are locked out (Auth).
  • You cannot lock yourself out — the server refuses self-deactivation and self-demotion from site admin.
  • Bulk edit is tri-state — editing N users writes only the fields you explicitly set; "keep" is the default for everything.
  • Every mutation is audited — check /admin/audit after any admin action.

Adding a section

  1. Server functions in example/src/lib/admin-<entity>-server.ts, each opening with requireSiteAdmin().
  2. A section component in example/src/components/site-admin/ — columns for DataTable, actions in SelectionBar, optionally an EditSheet.
  3. A route _authed.admin.<entity>.tsx rendering the section; add it to the NAV list in the layout.
  4. Extend the journey spec (e2e/site-admin.spec.ts) to walk through the section.