Skip to Content

Admin UI

@quickben/admin renders an admin UI from your config: a dashboard, list views and create and edit forms. Pages are React Server Components. Writes go through one server function.

Posts list in the admin UI

pnpm add @quickben/admin @quickben/next

Setup

Two files:

app/admin/[[...segments]]/page.tsx
import { createAdminPage, handleAdminRequest } from "@quickben/admin"; import config from "../../../quickben.config"; import type { AdminRequest } from "@quickben/admin"; async function serverFunction(request: AdminRequest) { "use server"; return handleAdminRequest(config, request); } export default createAdminPage(config, { serverFunction });
app/admin/layout.tsx
import "@quickben/admin/styles.css"; import type { ReactNode } from "react"; export default function AdminLayout({ children }: Readonly<{ children: ReactNode }>) { return children; }

Why the page file defines serverFunction

Saves, deletes and relation searches call serverFunction. Next.js runs a server function by loading the module that defines it.

serverFunctionModule Next.js loadsAfter a restart or on a new serverless instance
Defined in your page fileYour page file, with configWorks.
OmittedA module inside @quickben/adminThe config is unknown until the page renders once. A form submitted from an open tab fails with “Reload the page”.

createAdminPage(config) without options works in development. Pass serverFunction in production.

handleAdminRequest checks the shape of the request and runs it through the local API as the current user. Access rules and hooks apply.

Routes

PathView
/adminDashboard: readable collections with counts, grouped like the sidebar.
/admin/:collectionList: columns, search, filters, sort, pagination.
/admin/:collection/newCreate form.
/admin/:collection/:idEdit form, details, delete.
/admin/:page/...Custom pages.
User canAdmin shows
Read a collectionSidebar entry, dashboard card, list and edit views.
Not updateRead-only forms.
Create / delete“New” button / “Delete” button.
Nothing (anonymous)A link to signInHref, when set.

The user comes from config.auth on every request.

Admin dashboard

List view

FeatureSourceURL
Columnsadmin.defaultColumns, else the title field and the next three simple fields.
Searchcontains on admin.useAsTitle when it is a text field.?q=ada
FiltersVisible select and boolean fields.?filter.status=draft
SortClick a column header.?sort=-title
Pagination10, 20, 50 or 100 per page.?page=2&limit=50

Relation columns show the title of the related document.

Forms

Edit form

Field typeInput
text, email, url<input> of that type, with length limits
textarea<textarea>
number<input type="number">, step 1 for integers
booleanCheckbox
date / datetimedate / datetime-local input (UTC)
select / select many<select> / checkbox group
relation / relation manySearch box with keyboard support, chips for many
jsonTextarea with live syntax check and Format button
Field optionEffect
labelLabel text.
descriptionHint text under the input.
admin.hiddenNot rendered, not sent.
admin.readOnlyShown disabled, not sent.
admin.widthfull, half or third of the row.
admin.componentName of a custom component.

Validation errors from the local API are shown next to their field. The first invalid field gets focus.

Config the admin reads

OptionWhereEffect
admin.titledefineConfigTitle in the sidebar and dashboard.
labelsdefineCollectionSingular and plural names.
admin.useAsTitledefineCollectionDocument title, search field, relation labels.
admin.defaultColumnsdefineCollectionList columns.
admin.descriptiondefineCollectionText under the list title.
admin.groupdefineCollectionSidebar group. Default “Collections”.
admin.hiddendefineCollectionHide from the sidebar and dashboard.
modulespluginsSidebar groups. See modules.

createAdminPage options

OptionDefaultPurpose
serverFunctionbuilt-inServer function for saves, deletes and relation searches.
basePath/adminURL path of the admin route.
signInHrefnoneLink shown to anonymous users who cannot read anything.
components{}Custom field components by name.
views{}Replace the list or edit view of a collection, by slug.
pages[]Custom pages inside the admin shell.
nav[]Extra sidebar links.
extensions[]{ components, views, pages, nav } from plugins. Applied first.

Extensions

app/admin/[[...segments]]/page.tsx
import { createAdminPage, handleAdminRequest } from "@quickben/admin"; import config from "../../../quickben.config"; import { ColorInput } from "./color-input"; import { PostBoard } from "./post-board"; import { Reports } from "./reports"; import type { AdminRequest } from "@quickben/admin"; async function serverFunction(request: AdminRequest) { "use server"; return handleAdminRequest(config, request); } export default createAdminPage(config, { serverFunction, components: { color: ColorInput }, views: { posts: { list: PostBoard } }, pages: [{ path: "reports", label: "Reports", component: Reports }], nav: [{ label: "Docs", href: "https://quickben.vialabs.net", group: "Help" }], signInHref: "/sign-in", });
OptionComponent props
componentsFieldComponentProps: field, name, id, value, required, disabled, describedBy
viewsViewProps: collection, user, basePath, searchParams, id?
pagesPageProps: user, basePath, segments, searchParams

Props are plain data, so components can be server or client components.

Custom field components

A field picks a component with admin: { component: "color" }. The component renders form controls named name. The admin reads their values like the built-in input of that field type: a string for text, JSON text for json.

color: f.text({ admin: { component: "color" } }),
app/admin/[[...segments]]/color-input.tsx
import type { FieldComponentProps } from "@quickben/admin"; export function ColorInput({ name, id, value, required, disabled, describedBy, }: FieldComponentProps) { return ( <input type="color" name={name} id={id} defaultValue={typeof value === "string" ? value : "#000000"} required={required} disabled={disabled} aria-describedby={describedBy} /> ); }

Custom views and pages

app/admin/[[...segments]]/post-board.tsx
import type { ViewProps } from "@quickben/admin"; export function PostBoard({ collection, basePath }: ViewProps) { return <a href={`${basePath}/${collection.slug}/new`}>New {collection.labels.singular}</a>; }
app/admin/[[...segments]]/reports.tsx
import { Card, CardBody, CardHeader } from "@quickben/admin/ui"; import type { PageProps } from "@quickben/admin"; export function Reports({ user, segments }: PageProps) { return ( <Card> <CardHeader title="Reports" description={`Signed in as ${user?.email ?? "anonymous"}`} /> <CardBody>Path: /{segments.join("/")}</CardBody> </Card> ); }

pages[].path is the first URL segment: lowercase letters, digits and -. It must not be a collection slug.

  1. Modules: their nav, or their collections.
  2. Other collections, by admin.group.
  3. Custom pages, by group (default “Pages”).
  4. nav links.

Styles

@quickben/admin/styles.css is built with Tailwind CSS v4. Import it once, in app/admin/layout.tsx. You do not need Tailwind in your app.

RuleEffect
qb: prefixAdmin utilities do not clash with your own Tailwind classes.
--qb-* variablesTheme variables do not clash with yours.
No global resetBase styles apply only inside .qb-root, the element around the admin.
Tailwind v4 in your appWorks with no change. Your @source does not need to scan this package.

The admin styles are in CSS cascade layers. Unlayered CSS in your app wins over them. A global reset such as * { margin: 0; padding: 0 } outside a layer removes the admin’s spacing. Put such rules in @layer base { … }.

@quickben/admin/ui exports the component kit: Button, ButtonLink, Input, Textarea, Select, Checkbox, Label, Card, CardHeader, CardBody, Badge, Table, Toast. Use it in custom components to match the admin.

Limits

LimitEffect
Document id newCannot be opened. /admin/:collection/new is the create form.
DatetimesEdited in UTC.
Cache Components Not supported yet. next build fails on the admin page.
Page <title>Not set by the admin. Set metadata in app/admin/layout.tsx.
Last updated on