Admin

The root provider that wires the data, query, i18n, store, and router stack.

Installation

pnpm dlx shadcn@latest add @chelone/admin

<Admin> is the root of a Chelone app. It layers the whole provider stack — the data provider, the query client, optional i18n, notifications, store, auth, and the resource registry — then renders the resources and routes you declare as children. It joins the router your app already runs, or creates its own, so a wrapper router is optional.

The minimal admin

src/App.tsx
import { fakeDataProvider } from "@chelone/core";
import { Admin, Layout, Resource } from "@chelone/react";
import { QueryClient } from "@tanstack/react-query";
import { PostList } from "./posts";
 
const queryClient = new QueryClient();
const dataProvider = fakeDataProvider({ posts: [] });
 
export function App() {
  return (
    <Admin dataProvider={dataProvider} queryClient={queryClient}>
      <Admin.Layout>
        <Layout />
      </Admin.Layout>
      <Resource name="posts" list={PostList} />
    </Admin>
  );
}

dataProvider and queryClient are the only required props. The rest are opt-in services you add as the app grows. With no host router, <Admin> creates a browser router for you.

The full stack

A production admin configures the optional services too — the i18n catalog, the notification store the toaster reads, and the KV store for list scroll and preferences:

import {
  createNotificationStore,
  createStore,
  fakeDataProvider,
} from "@chelone/core";
import { Admin, Layout, Resource } from "@chelone/react";
import { QueryClient } from "@tanstack/react-query";
import { makeI18n } from "./i18n";
import { PostEdit, PostList } from "./posts";
import { resources } from "./resources";
 
const queryClient = new QueryClient();
const dataProvider = fakeDataProvider({ posts: [] });
const i18n = makeI18n();
const notifications = createNotificationStore();
const store = createStore();
 
export function App() {
  return (
    <Admin
      dataProvider={dataProvider}
      queryClient={queryClient}
      i18n={i18n}
      notifications={notifications}
      store={store}
      resources={resources}
    >
      <Admin.Layout>
        <Layout />
      </Admin.Layout>
      <Resource name="posts" list={PostList} edit={PostEdit} />
    </Admin>
  );
}

The resources prop and schema validation

Pass a resource registry built with defineResources({ posts: { schema } }) as resources to enable automatic form validation — the form controllers read each resource's Zod schema and validate against it on submit. See Resource for the registry.

The layout slot

<Admin.Layout> is the opt-in shell: sidebar, app bar, toaster. Omit it for a headless admin, or pass your own layout element. Other slots (<Admin.Dashboard>, <Admin.LoginPage>, and the auth/error pages) follow the same pattern.

Each slot also has a prop of the same lowercase name (layout, dashboard, loginPage, catchAll, and so on). When both are given, the prop wins over the slot child:

import { Admin, Resource } from "@chelone/react";
import { Home, OldHome } from "./home";
import { dataProvider, queryClient } from "./providers";
import { PostList } from "./posts";
 
export function App() {
  return (
    <Admin
      dataProvider={dataProvider}
      queryClient={queryClient}
      dashboard={<Home />}
    >
      <Admin.Dashboard>
        <OldHome />
      </Admin.Dashboard>
      <Resource name="posts" list={PostList} />
    </Admin>
  );
}

Here <Home /> renders at the index route and <OldHome /> never does.

Auth, basename and permissions

  • requireAuth gates the whole shell on authProvider. While the auth check is in flight the loading page shows, never the layout. An unauthenticated visitor gets the login page. Without an authProvider the gate is open.
  • basename is the URL prefix the app is served under. A router <Admin> creates applies it, so links and redirects come out prefixed once. Under a host router, set it to the path the host mounts <Admin> at, relative to the host router's own basename, and Chelone prepends it to the links it builds.
  • The children can be a function that receives the permissions prop, so resources can depend on who is signed in. It may return a promise, and the loading page shows until it resolves. A ready slot, if you set one, shows in its place during that wait.
src/App.tsx
import { Admin, Resource } from "@chelone/react";
import { authProvider, dataProvider, queryClient } from "./providers";
import { PostList } from "./posts";
import { UserList } from "./users";
 
export function App({ permissions }: { permissions: string }) {
  return (
    <Admin
      dataProvider={dataProvider}
      queryClient={queryClient}
      authProvider={authProvider}
      requireAuth
      permissions={permissions}
    >
      {(role) => (
        <>
          <Resource name="posts" list={PostList} />
          {role === "admin" ? (
            <Resource name="users" list={UserList} />
          ) : null}
        </>
      )}
    </Admin>
  );
}

Swapping the router

<Admin> defaults to the react-router adapter. Pass a different adapter on router to run under another router. The components speak the adapter's navigation contract, not a specific router.

@chelone/react/tanstack-router ships a TanStack Router adapter. It is a reference adapter that shows the router seam is neutral, not a second maintained router. It creates its own TanStack router on memory history, so the browser URL bar does not follow navigation, and it does not join an existing TanStack router. Its custom routes must use the adapter's own Route, not react-router's:

src/App.tsx
import { fakeDataProvider } from "@chelone/core";
import { Admin, CustomRoutes, Layout, Resource } from "@chelone/react";
import { tanstackRouterAdapter } from "@chelone/react/tanstack-router";
import { QueryClient } from "@tanstack/react-query";
import { PostList } from "./posts";
import { SettingsPage } from "./settings";
 
const queryClient = new QueryClient();
const dataProvider = fakeDataProvider({ posts: [] });
const { Route } = tanstackRouterAdapter;
 
export function App() {
  return (
    <Admin
      dataProvider={dataProvider}
      queryClient={queryClient}
      router={tanstackRouterAdapter}
    >
      <Admin.Layout>
        <Layout />
      </Admin.Layout>
      <Resource name="posts" list={PostList} />
      <CustomRoutes>
        <Route path="/settings" element={<SettingsPage />} />
      </CustomRoutes>
    </Admin>
  );
}

The adapter lives on its own entry so @chelone/react loads without @tanstack/react-router, an optional peer. Install that peer only when you use this adapter.

Props

PropTypeDefaultDescription
accessDeniedReactNode—
authCallbackReactNode—
authenticationErrorReactNode—
authProviderAuthProvider—The optional auth provider (gates the shell when `requireAuth`).
basenamestring—The URL prefix the app is served under (default `''`). When `<Admin>` creates its router, that router applies it. Under a host router, it is the path the host mounts `<Admin>` at, relative to the host router's own basename; Chelone prepends it to the links it builds.
catchAllReactNode—
childrenAdminChildren—Resources / custom routes / page-slot markers — OR a single `(permissions) => node | Promise<node>` function-child (parity §0a-9).
dashboardReactNode—
dataProviderDataProvider<TResources>—The data provider (required — the P2 data seam).
errorReactNode—
i18nAnyCheloneI18n—The optional i18n instance (translations).
layoutReactNode—
loadingReactNode—
loginPageReactNode—
notificationsNotificationStore—The optional notification store (i18n-key notifications).
permissionsunknown—The app's permissions, passed to the function-child + registration.
queryClientQueryClient—The React Query client (required — factory-created at the app root).
readyReactNode—
registryResourceRegistry—An existing runtime resource registry. Omit to have the provider create one (the common case). Supply one to share a registry across trees / for SSR.
requireAuthboolean—Gate the shell on auth (the no-flash gate — consumed by W1-T7).
resourcesTResources—The zod resource registry (enables registry-schema form validation).
routerRouterAdapter—The router adapter (defaults to `reactRouterAdapter`). Named `router` here (the app-facing name); mapped to `<AdminContext>`'s `adapter` prop.
storeCheloneStore—The optional KV store (preferences / list scroll).
titleReactNode—

Tips

  • With the default react-router adapter, <Admin> joins a router above it (<BrowserRouter>, or your framework's router) and creates its own browser router when there is none.
  • Create the QueryClient and the data provider at module scope (stable across renders), not inside the component body.