Admin API
Admin conventions and client API reference — fields, views, pages, widgets, and configuration.
File Conventions
The admin module discovers its server-side configuration from config/admin.ts in the QUESTPIE server directory.
import { adminConfig } from "#questpie/factories";
export default adminConfig({
sidebar: {
sections: [
{ id: "content", title: { en: "Content", sk: "Obsah" } },
{ id: "settings", title: { en: "Settings" } },
],
items: [
{ sectionId: "content", type: "collection", collection: "posts" },
{ sectionId: "settings", type: "global", global: "siteSettings" },
{
sectionId: "settings",
type: "link",
label: { en: "Documentation" },
href: "https://docs.example.com",
external: true,
icon: { type: "icon", props: { name: "ph:book-open" } },
},
],
},
dashboard: {
title: { en: "Dashboard" },
description: { en: "Overview of your content" },
columns: 3,
realtime: true,
sections: [
{ id: "stats", label: "Quick Stats" },
{ id: "recent", label: "Recent Activity" },
],
items: [
{
sectionId: "stats",
id: "total-posts",
type: "stats",
collection: "posts",
label: "Total Posts",
},
{
sectionId: "recent",
id: "recent-posts",
type: "recentItems",
collection: "posts",
limit: 5,
dateField: "createdAt",
label: "Recent Posts",
},
],
},
branding: {
name: { en: "My App", sk: "Moja aplikacia" },
},
locale: {
locales: ["en", "sk"],
defaultLocale: "en",
},
});Dashboard Item Types
| Type | Properties |
|---|---|
stats | collection, filter, label |
value | loader, label, refreshInterval |
progress | loader, label, showPercentage |
chart | collection, field, chartType, label |
recentItems | collection, limit, dateField, label |
timeline | loader, maxItems, label |
Shell Architecture
The admin panel has three independent ownership layers:
| Layer | Owner | Where |
|---|---|---|
| App shell | Your app | app/admin/layout.tsx (or equivalent) — wraps AdminRouter/AdminLayout with your router, auth client, QueryClientProvider, favicon, and CSS |
| Structure & navigation | Server config | config/admin.ts — sidebar sections, dashboard, branding name |
| Chrome overrides | Client file discovery | questpie/admin/components/ — reserved component files |
config/admin.ts responsibilities
config/admin.ts controls:
- Sidebar sections and navigation items
- Dashboard layout and widgets
- Brand name metadata
- Locale settings
It does not control favicon, CSS theme selection, or React-level chrome (sidebar HTML, auth page layout). Those belong in the app shell or file-first overrides.
File-first chrome overrides
Place one of these files in questpie/admin/components/ to override specific chrome pieces without touching the app shell:
| File | Registry key | What it replaces |
|---|---|---|
admin-sidebar-brand.tsx | adminSidebarBrand | Sidebar logo + name area |
admin-sidebar-nav-item.tsx | adminSidebarNavItem | Each navigation item row |
admin-auth-layout.tsx | adminAuthLayout | Auth page wrapper (login, reset, etc.) |
These files use the standard questpie/admin/components/ discovery pattern — they are plain React component files, not wrapped in a factory call.
Important: the default export should be a React component (sync or React.lazy(...)). Do not default-export a raw () => import("...") loader function for these reserved override files.
Example — custom sidebar brand:
import type { AdminSidebarBrandProps } from "@questpie/admin/client";
export default function MyBrand({ name, collapsed }: AdminSidebarBrandProps) {
return (
<div className="flex items-center gap-2">
<img src="/logo.svg" alt={name} className="size-6" />
{!collapsed && <span className="font-bold">{name}</span>}
</div>
);
}Example — custom auth layout:
import type { AuthLayoutProps } from "@questpie/admin/client";
export default function MyAuthLayout({
title,
children,
footer,
}: AuthLayoutProps) {
return (
<div className="min-h-screen flex items-center justify-center bg-zinc-950">
<div className="w-full max-w-sm space-y-4 p-6">
<h1 className="text-2xl font-bold text-white">{title}</h1>
{children}
{footer && <div className="text-sm text-zinc-400">{footer}</div>}
</div>
</div>
);
}Render priority for overrides
Each override slot follows the same three-tier priority:
- Runtime prop (e.g.
renderBrandon<AdminSidebar>) — app shell wins - File-first registry component — file in
questpie/admin/components/ - Built-in fallback — default QUESTPIE chrome
Client Factories
The admin client uses plain frozen factories to register field types, views, pages, and widgets.
field(name, config)
Register a field type for the admin UI. Maps a field type name to its form component and optional cell (list column) component.
import { field } from "@questpie/admin/client";
export default field("text", {
component: () => import("./text-field.js"),
cell: () => import("./cells/primitive-cells.js"),
});FieldDefinition shape:
| Property | Type | Description |
|---|---|---|
name | string | Field type name |
component | MaybeLazyComponent | Form field React component |
cell | MaybeLazyComponent | Optional list cell component |
view(name, config)
Register a view type. Views have a kind discriminant: "list" for collection list pages, "form" for edit/create pages.
import { view } from "@questpie/admin/client";
export default view("collection-table", {
kind: "list",
component: () => import("./table-view.js"),
});
export default view("collection-form", {
kind: "form",
component: () => import("./form-view.js"),
});ViewDefinition shape:
| Property | Type | Description |
|---|---|---|
name | string | View type name |
kind | "list" | "form" | View kind discriminant |
component | MaybeLazyComponent | React component |
widget(name, config)
Register a dashboard widget.
import { widget } from "@questpie/admin/client";
export default widget("stats", {
component: () => import("./stats-widget.js"),
});WidgetDefinition shape:
| Property | Type | Description |
|---|---|---|
name | string | Widget identifier |
component | MaybeLazyComponent | React component |
configureField(base, options)
Bridges a field registry entry (FieldDefinition) to a runtime instance (FieldInstance) by combining it with server-provided options.
import { configureField } from "@questpie/admin/client";
const instance = configureField(textFieldDef, {
label: "Title",
required: true,
placeholder: "Enter title...",
});This is used internally when mapping server introspection metadata to field rendering. You typically don't call this directly.
AdminState
The admin client state is a flat map containing all registered entities:
interface AdminState {
fields: Record<string, FieldDefinition>;
views: Record<string, ViewDefinition>;
pages: Record<string, PageDefinition>;
widgets: Record<string, WidgetDefinition>;
components: Record<string, MaybeLazyComponent>;
blocks: Record<string, MaybeLazyComponent>;
translations: Record<string, Record<string, string>>;
locale: string;
}The admin client config is auto-generated by codegen into .generated/client.ts. No manual setup is needed:
import admin from "@/questpie/admin/.generated/client";The generated config includes all registered fields, views, components, blocks, and built-in defaults.
Form View Configuration
Form Layout
v.collectionForm({
sidebar: {
position: "right",
fields: [f.status, f.publishedAt],
},
fields: [
f.title,
f.slug,
{
type: "section",
label: { en: "Content" },
description: { en: "Main content area" },
layout: "stack",
fields: [f.content, f.excerpt],
},
],
});Section Options
| Property | Type | Description |
|---|---|---|
type | "section" | Section discriminant |
label | string | i18n | Section heading |
description | string | i18n | Section description |
layout | "grid" | "stack" | Field layout within section |
columns | number | Grid columns (grid layout) |
fields | (Field | FieldConfig)[] | Fields in this section |
Field Config Options
Override field behavior per-form:
{
field: f.slug,
hidden: ({ data }) => !data.title,
readOnly: ({ data }) => data.status === "published",
compute: {
handler: ({ data }) => slugify(data.title),
deps: ({ data }) => [data.title],
debounce: 300,
},
}