@questpie/admin
v3.0.0
Published
Server-driven admin UI for QUESTPIE. Reads your server schema via introspection and generates a complete admin panel — dashboard, table views, form editors, sidebar navigation, block editor — all from the definitions you already wrote on the server.
Readme
@questpie/admin
Server-driven admin UI for QUESTPIE. Reads your server schema via introspection and generates a complete admin panel — dashboard, table views, form editors, sidebar navigation, block editor — all from the definitions you already wrote on the server.
How It Works
QUESTPIE follows a server-first architecture. All schema, layout, and behavior is defined on the server with the questpie core package. The admin UI consumes this via introspection — no duplicate config needed on the client.
| Layer | Package | Defines |
| ---------- | ------------------------------------- | ----------------------------------------------------------------- |
| Server | questpie + @questpie/admin/server | Schema, fields, access, hooks, sidebar, dashboard, branding |
| Client | @questpie/admin/client | Field renderers, view renderers, component registry, UI overrides |
Installation
bun add @questpie/admin questpie @questpie/tanstack-query @tanstack/react-queryServer Setup
The admin plugin is registered in questpie.config.ts, and the admin module is added to modules.ts:
// questpie.config.ts
import { runtimeConfig } from "questpie";
import { adminPlugin } from "@questpie/admin/plugin";
export default runtimeConfig({
plugins: [adminPlugin()],
app: { url: process.env.APP_URL! },
db: { url: process.env.DATABASE_URL! },
secret: process.env.AUTH_SECRET!,
});// modules.ts
import { adminModule } from "@questpie/admin/server";
export default [adminModule] as const;Branding, sidebar, and dashboard are configured via the file convention (e.g. branding.ts, sidebar.ts, dashboard.ts) and auto-discovered by codegen.
Collections, globals, routes, and jobs are auto-discovered via file convention. Codegen produces a .generated/index.ts with the fully-typed App and runtime app instance.
Collection Admin Config
Admin metadata, list views, and form views are defined on the collection itself:
const posts = collection("posts")
.fields(({ f }) => ({
title: f.text(255).required().label("Title"),
content: f.richText().label("Content"),
status: f.select(["draft", "published"]).label("Status"),
cover: f.upload({ to: "assets", mimeTypes: ["image/*"] }),
publishedAt: f.date(),
}))
.title(({ f }) => f.title)
.admin(({ c }) => ({
label: { en: "Blog Posts" },
icon: c.icon("ph:article"),
}))
.list(({ v, f }) =>
v.collectionTable({
columns: [f.title, f.status, f.publishedAt],
}),
)
.form(({ v, f }) =>
v.collectionForm({
layout: "with-sidebar",
sidebar: {
position: "right",
fields: [f.status, f.publishedAt, f.cover],
},
fields: [
{ type: "section", label: "Content", fields: [f.title, f.content] },
],
}),
);Component References
The server emits serializable ComponentReference objects instead of React elements. Use the c factory in admin config callbacks:
.admin(({ c }) => ({
icon: c.icon("ph:article"), // → { type: "icon", props: { name: "ph:article" } }
badge: c.badge({ text: "New" }), // → { type: "badge", props: { text: "New" } }
}))Icons use the Iconify format with the Phosphor set (ph:icon-name).
Form Layouts
// Sections
.form(({ v, f }) => v.collectionForm({
fields: [
{ type: "section", label: "Basic Info", layout: "grid", columns: 2,
fields: [f.name, f.email, f.phone, f.city] },
{ type: "section", label: "Content", fields: [f.body] },
],
}))
// Sidebar layout
.form(({ v, f }) => v.collectionForm({
layout: "with-sidebar",
sidebar: { position: "right", fields: [f.status, f.image] },
fields: [f.title, f.content],
}))
// Tabs
.form(({ v, f }) => v.collectionForm({
tabs: [
{ id: "content", label: "Content", fields: [f.title, f.body] },
{ id: "meta", label: "Metadata", fields: [f.seoTitle, f.seoDescription] },
],
}))Reactive Fields
Server-evaluated reactive behaviors in form config:
.form(({ v, f }) => v.collectionForm({
fields: [
{
field: f.slug,
compute: {
handler: ({ data }) => slugify(data.title),
deps: ({ data }) => [data.title],
debounce: 300,
},
},
{ field: f.publishedAt, hidden: ({ data }) => !data.published },
{ field: f.reason, readOnly: ({ data }) => data.status !== "cancelled" },
],
}))Dashboard Actions
.dashboard(({ d, c, a }) => d.dashboard({
actions: [
a.create({ collection: "posts", label: { en: "New Post" }, icon: c.icon("ph:plus"), variant: "primary" }),
a.global({ global: "siteSettings", label: { en: "Settings" }, icon: c.icon("ph:gear-six") }),
a.link({ href: "/", label: { en: "Open Site" }, icon: c.icon("ph:arrow-square-out"), variant: "outline" }),
],
items: [
{ type: "section", items: [
{ type: "stats", collection: "posts", label: "Posts" },
]},
],
}))Client Setup
The client creates a typed admin builder and mounts the admin UI in React:
1. Admin Builder
// questpie/admin/builder.ts
import { qa, adminModule } from "@questpie/admin/client";
import type { App } from "#questpie";
export const admin = qa<App>().use(adminModule);Note: The old
.toNamespace()call has been removed.qa<App>().use(adminModule)is the complete builder.
2. Typed Hooks
// questpie/admin/hooks.ts
import { createTypedHooks } from "@questpie/admin/client";
import type { App } from "#questpie";
export const {
useCollectionList,
useCollectionItem,
useCollectionCreate,
useCollectionUpdate,
useCollectionDelete,
useGlobal,
useGlobalUpdate,
} = createTypedHooks<App>();3. Mount in React
// routes/admin.tsx
import { AdminRouter } from "@questpie/admin/client";
import { admin } from "@/questpie/admin/builder";
import { appClient } from "@/lib/client";
import { queryClient } from "@/lib/query-client";
export default function AdminRoute() {
return (
<AdminRouter
admin={admin}
client={client}
queryClient={queryClient}
basePath="/admin"
/>
);
}4. Tailwind CSS
Import admin styles and scan the admin package:
@import "tailwindcss";
@import "@questpie/admin/styles/index.css";
@source "../node_modules/@questpie/admin/dist";Block Editor
The admin includes a full drag-and-drop block editor. Blocks are defined server-side:
const heroBlock = block("hero")
.admin(({ c }) => ({
label: { en: "Hero Section" },
icon: c.icon("ph:image"),
category: { label: "Sections", icon: c.icon("ph:layout") },
}))
.fields(({ f }) => ({
title: f.text({ required: true }),
subtitle: f.textarea(),
backgroundImage: f.upload({ to: "assets", mimeTypes: ["image/*"] }),
}))
.prefetch({ with: { backgroundImage: true } });Render blocks on the client with BlockRenderer:
import { BlockRenderer, createBlockRegistry } from "@questpie/admin/client";
const registry = createBlockRegistry({
hero: ({ block }) => (
<section style={{ backgroundImage: `url(${block.backgroundImage?.url})` }}>
<h1>{block.title}</h1>
<p>{block.subtitle}</p>
</section>
),
});
function Page({ blocks }) {
return <BlockRenderer blocks={blocks} registry={registry} />;
}Actions System
Collection-level actions with multiple handler types:
| Type | Description |
| ---------- | ------------------------------------------- |
| navigate | Client-side routing |
| api | HTTP API call |
| form | Dialog with field inputs |
| server | Server-side execution with full app context |
| custom | Arbitrary client-side code |
Actions can be scoped to header (list toolbar), bulk (selected items), single (per-item), or row.
Realtime
SSE-powered live updates are enabled by default. Collection lists and dashboard widgets auto-refresh when data changes.
// Disable globally
<AdminRouter admin={admin} client={client} realtime={false} />
// Disable per collection view
.list(({ v }) => v.collectionTable({ realtime: false }))URL-Synced Panels
Admin keeps major panel states in URL search params so links are shareable and browser navigation works as expected.
sidebar=view-options- collection list view options sheetsidebar=history- version history sidebar (collection/global forms)sidebar=block-library- block editor library sidebarpreview=true- live preview mode (collection form)
Legacy params (history, viewOptions, and sidebar=preview) are still read for backward compatibility.
Package Exports
// Client (React components, builder, hooks)
import {
qa,
adminModule,
AdminRouter,
createTypedHooks,
BlockRenderer,
createBlockRegistry,
} from "@questpie/admin/client";
// Server (admin module + plugin for QUESTPIE config)
import { adminModule, auditModule } from "@questpie/admin/server";
import { adminPlugin } from "@questpie/admin/plugin";
// Styles
import "@questpie/admin/styles/index.css";Component Stack
- Primitives:
@base-ui/react(NOT Radix) - Icons:
@iconify/reactwith Phosphor set (ph:icon-name) - Styling: Tailwind CSS v4 + shadcn components
- Toast:
sonner
Documentation
Full documentation: https://questpie.com/docs/admin
License
MIT
