QUESTPIE

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.

config/admin.ts
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

TypeProperties
statscollection, filter, label
valueloader, label, refreshInterval
progressloader, label, showPercentage
chartcollection, field, chartType, label
recentItemscollection, limit, dateField, label
timelineloader, maxItems, label

Shell Architecture

The admin panel has three independent ownership layers:

LayerOwnerWhere
App shellYour appapp/admin/layout.tsx (or equivalent) — wraps AdminRouter/AdminLayout with your router, auth client, QueryClientProvider, favicon, and CSS
Structure & navigationServer configconfig/admin.ts — sidebar sections, dashboard, branding name
Chrome overridesClient file discoveryquestpie/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:

FileRegistry keyWhat it replaces
admin-sidebar-brand.tsxadminSidebarBrandSidebar logo + name area
admin-sidebar-nav-item.tsxadminSidebarNavItemEach navigation item row
admin-auth-layout.tsxadminAuthLayoutAuth 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:

questpie/admin/components/admin-sidebar-brand.tsx
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:

questpie/admin/components/admin-auth-layout.tsx
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:

  1. Runtime prop (e.g. renderBrand on <AdminSidebar>) — app shell wins
  2. File-first registry component — file in questpie/admin/components/
  3. 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:

PropertyTypeDescription
namestringField type name
componentMaybeLazyComponentForm field React component
cellMaybeLazyComponentOptional 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:

PropertyTypeDescription
namestringView type name
kind"list" | "form"View kind discriminant
componentMaybeLazyComponentReact 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:

PropertyTypeDescription
namestringWidget identifier
componentMaybeLazyComponentReact 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

PropertyTypeDescription
type"section"Section discriminant
labelstring | i18nSection heading
descriptionstring | i18nSection description
layout"grid" | "stack"Field layout within section
columnsnumberGrid 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,
  },
}

On this page