| name | rebase-admin |
| description | Guide for navigating the Rebase admin CMS, opening entities in side drawers, building URLs, embedding collection panels, using the collection registry, and programmatic navigation. Use this skill when an agent or user needs to navigate to a collection view, open a entity in the side panel/drawer, build admin URLs, embed a collection inside a custom page, use the entity selection dialog, or access CMS-specific controllers. |
Rebase Admin (@rebasepro/admin)
The @rebasepro/admin package provides the CMS layer for Rebase. It handles collection views, entity editing, navigation, side panels (drawers), URL routing, breadcrumbs, and the full CMS context. This skill covers the programmatic APIs for navigating and interacting with the admin.
IMPORTANT FOR AGENTS: All hooks in this skill must be called inside the <RebaseShell> component tree. They rely on React contexts provided by <RebaseNavigation>, <SideEntityProvider>, and <RebaseRouteDefs>.
Building the view itself? This skill covers navigation and CMS plumbing. For what the view should look like, read the rebase-design-language skill first — custom views render inside the admin shell and must match it. It ships whole-view skeletons and points at the live UI reference at /debug/ui.
Quick Reference — Common Tasks
| Task | Hook / Component | Package |
|---|
| Open entity in side drawer | useSidePanel() | @rebasepro/admin |
| Navigate to a collection view | useUrlController() | @rebasepro/admin |
| Look up a collection by slug | useCollectionRegistryController() | @rebasepro/admin |
| Embed a collection in a custom page | <CollectionPanel> | @rebasepro/admin |
| Add custom top-level views | <RebaseAdmin views={[...]}> | @rebasepro/admin |
| Replace how one property is edited or shown | admin: { Field, Preview } | see §12 |
| Open a entity selection dialog | useSelectionDialog() | @rebasepro/admin |
| Open a custom side dialog | useSideDialogsController() | @rebasepro/admin |
| Set breadcrumbs | useBreadcrumbsController() | @rebasepro/admin |
| Access full CMS context | useCMSContext() | @rebasepro/admin |
| Access navigation state & views | useNavigationStateController() | @rebasepro/admin |
1. Opening Entities in the Side Drawer
Use useSidePanel() to open, replace, or close entity side panels (the sliding drawer that shows entity forms).
import { useSidePanel } from "@rebasepro/admin";
SidePanelController Interface
interface SidePanelController {
close: () => void;
open: <M extends Record<string, unknown>>(props: EntitySidePanelProps<M>) => void;
replace: <M extends Record<string, unknown>>(props: EntitySidePanelProps<M>) => void;
}
EntitySidePanelProps
interface EntitySidePanelProps<M extends Record<string, unknown> = Record<string, unknown>> {
path: string;
entityId?: string | number;
copy?: boolean;
selectedTab?: string;
width?: number | string;
collection?: CollectionConfig<M>;
updateUrl?: boolean;
onUpdate?: (params: { entity: Entity<M> }) => void;
onClose?: () => void;
closeOnSave?: boolean;
formProps?: Record<string, unknown>;
allowFullScreen?: boolean;
defaultValues?: Partial<M>;
}
Examples
Open an existing entity for editing:
const sideEntityController = useSidePanel();
sideEntityController.open({
path: "products",
entityId: "abc123"
});
Create a new entity with pre-filled values:
sideEntityController.open({
path: "products",
defaultValues: {
name: "New Product",
status: "draft"
}
});
Open with a callback on save:
sideEntityController.open({
path: "orders",
entityId: orderId,
closeOnSave: true,
onUpdate: ({ entity }) => {
console.log("Saved:", entity.id, entity.values);
}
});
Replace the current panel (instead of stacking):
sideEntityController.replace({
path: "clients",
entityId: "xyz789"
});
2. Navigating to Collection Views
Use useUrlController() to build URLs and navigate programmatically within the admin.
import { useUrlController } from "@rebasepro/admin";
UrlController Interface
type UrlController = {
basePath: string;
baseCollectionPath: string;
urlPathToDataPath: (cmsPath: string) => string;
homeUrl: string;
isUrlCollectionPath: (urlPath: string) => boolean;
buildUrlCollectionPath: (path: string) => string;
buildAppUrlPath: (path: string) => string;
resolveDatabasePathsFrom: (path: string) => string;
navigate: (to: string, options?: NavigateOptions) => void;
};
Examples
Navigate to a collection view:
const urlController = useUrlController();
const url = urlController.buildUrlCollectionPath("products");
urlController.navigate(url);
Navigate to a specific entity within a collection:
const url = urlController.buildUrlCollectionPath("products/abc123");
urlController.navigate(url);
Navigate to a custom view:
const url = urlController.buildAppUrlPath("my-dashboard");
urlController.navigate(url);
Navigate and replace history (no back button):
urlController.navigate(
urlController.buildUrlCollectionPath("orders"),
{ replace: true }
);
Navigate to home:
urlController.navigate(urlController.homeUrl);
IMPORTANT FOR AGENTS: The URL structure is basePath + baseCollectionPath + "/" + slug. By default this produces /c/products. Use buildUrlCollectionPath() instead of manually constructing URLs.
3. Collection Registry
Use useCollectionRegistryController() to look up registered collections by slug.
import { useCollectionRegistryController } from "@rebasepro/admin";
CollectionRegistryController Interface
type CollectionRegistryController<
DB = Record<string, unknown>,
EC extends CollectionConfig = CollectionConfig
> = {
collections?: CollectionConfig[];
initialised: boolean;
getCollection: <K extends keyof DB>(slugOrPath: Extract<K, string>, includeUserOverride?: boolean) => EC | undefined;
getRawCollection: (slugOrPath: string) => EC | undefined;
getParentReferencesFromPath: (path: string) => EntityReference[];
getParentCollectionSlugs: (path: string) => string[];
getParentEntityIds: (path: string) => string[];
convertIdsToPaths: (ids: string[]) => string[];
};
Example
const registry = useCollectionRegistryController();
const products = registry.getCollection("products");
if (products) {
console.log("Collection name:", products.name);
console.log("Properties:", Object.keys(products.properties));
}
registry.collections?.forEach(c => {
console.log(c.slug, c.name);
});
4. Embedding Collections with CollectionPanel
CollectionPanel is a high-level wrapper for embedding collection views inside custom pages (dashboards, home pages, entity detail views).
import { CollectionPanel } from "@rebasepro/admin";
CollectionPanelProps
type CollectionPanelProps = {
path: string;
title?: string | false;
viewMode?: ViewMode;
sort?: [string, "asc" | "desc"];
limit?: number;
updateUrl?: boolean;
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
className?: string;
collectionOverrides?: Partial<CollectionConfig>;
};
Examples
<CollectionPanel path="tasks" title="Pending Tasks" />
<CollectionPanel
path="clients"
viewMode="table"
limit={10}
sort={["createdAt", "desc"]}
/>
<CollectionPanel
path="orders"
title={false}
openEntityMode="dialog"
collectionOverrides={{
defaultFilter: { status: ["!=", "completed"] }
}}
/>
IMPORTANT FOR AGENTS: CollectionPanel defaults updateUrl to false so embedded panels don't hijack the browser URL. Only set updateUrl={true} if you explicitly need URL sync.
5. Entity Selection Dialog
Use useSelectionDialog() to open a side dialog for selecting entities (same mechanism used by reference fields).
import { useSelectionDialog } from "@rebasepro/admin";
Usage
function MyComponent() {
const { open, close } = useSelectionDialog<Product>({
path: "products",
onSingleEntitySelected: (entity) => {
console.log("Selected:", entity.id);
close();
}
});
return <Button onClick={open}>Select Product</Button>;
}
The hook accepts all SelectionProps except path (which you pass separately), plus an onClose callback.
6. Side Dialogs (Generic)
Use useSideDialogsController() to open arbitrary side panels with custom React content. This is the lower-level mechanism behind entity side panels.
import { useSideDialogsController } from "@rebasepro/admin";
SideDialogsController Interface
interface SideDialogsController {
close: () => void;
sidePanels: SideDialogPanelProps[];
setSidePanels: (panels: SideDialogPanelProps[]) => void;
open: (panelProps: SideDialogPanelProps | SideDialogPanelProps[]) => void;
replace: (panelProps: SideDialogPanelProps | SideDialogPanelProps[]) => void;
}
SideDialogPanelProps
interface SideDialogPanelProps {
key: string;
component: React.ReactNode;
width?: string;
urlPath?: string;
parentUrlPath?: string;
onClose?: () => void;
additional?: unknown;
}
Example
const sideDialogs = useSideDialogsController();
sideDialogs.open({
key: "my-custom-panel",
component: <MyCustomView data={someData} />,
width: "600px",
onClose: () => console.log("Panel closed")
});
7. Breadcrumbs
Use useBreadcrumbsController() to read or set the breadcrumb trail.
import { useBreadcrumbsController } from "@rebasepro/admin";
BreadcrumbsController Interface
interface BreadcrumbsController {
breadcrumbs: BreadcrumbEntry[];
set: (props: { breadcrumbs: BreadcrumbEntry[] }) => void;
updateCount: (id: string, count: number | null | undefined) => void;
}
interface BreadcrumbEntry {
title: string;
url: string;
count?: number | null;
id?: string;
}
Example
const breadcrumbs = useBreadcrumbsController();
breadcrumbs.set({
breadcrumbs: [
{ title: "Dashboard", url: "/" },
{ title: "Products", url: "/c/products", id: "products", count: null },
]
});
breadcrumbs.updateCount("products", 42);
8. Navigation State
Use useNavigationStateController() to access registered views, loading state, and trigger navigation refresh.
import { useNavigationStateController } from "@rebasepro/admin";
NavigationStateController Interface
type NavigationStateController = {
views?: AppView[];
adminViews?: AppView[];
topLevelNavigation?: NavigationResult;
loading: boolean;
navigationLoadingError?: unknown;
refreshNavigation: () => void;
plugins?: RebasePlugin[];
};
9. CMS Context (All-in-One)
Use useCMSContext() to get the full CMS context combining the core RebaseContext with all CMS-specific controllers.
import { useCMSContext } from "@rebasepro/admin";
CMSContext Type
type CMSContext = RebaseContext & {
sideEntityController: SidePanelController;
sideDialogsController: SideDialogsController;
urlController: UrlController;
navigationStateController: NavigationStateController;
collectionRegistryController: CollectionRegistryController;
};
Example
const context = useCMSContext();
context.sideEntityController.open({ path: "products", entityId: "abc" });
context.urlController.navigate(context.urlController.buildUrlCollectionPath("orders"));
context.collectionRegistryController.getCollection("products");
context.authController;
context.data;
TIP: Use useCMSContext() instead of useRebaseContext() when you need CMS controllers (side panels, navigation, URL). Use useRebaseContext() from @rebasepro/app when you only need core context (auth, data, storage).
10. Common Patterns
Navigate to a collection and open a entity
import { useUrlController, useSidePanel } from "@rebasepro/admin";
function navigateAndOpen() {
const urlController = useUrlController();
const sideEntityController = useSidePanel();
urlController.navigate(urlController.buildUrlCollectionPath("products"));
sideEntityController.open({
path: "products",
entityId: "abc123"
});
}
Open entity from a custom view without navigating
import { useSidePanel } from "@rebasepro/admin";
function MyCustomView() {
const sideEntityController = useSidePanel();
return (
<button onClick={() => sideEntityController.open({
path: "products",
entityId: "abc123",
updateUrl: false // don't change the URL
})}>
View Product
</button>
);
}
Programmatic entity creation from a custom view
import { useSidePanel } from "@rebasepro/admin";
function CreateButton() {
const sideEntity = useSidePanel();
return (
<button onClick={() => sideEntity.open({
path: "orders",
defaultValues: {
status: "pending",
createdAt: new Date()
},
closeOnSave: true,
onUpdate: ({ entity }) => {
console.log("Created order:", entity.id);
}
})}>
New Order
</button>
);
}
10. Custom Top-Level Views
Add custom pages to the main CMS navigation using the views prop on <RebaseAdmin>. Views appear alongside collections in the sidebar and home page.
AppView Interface
interface AppView {
slug: string;
name: string;
view: React.ReactNode;
icon?: string | React.ReactNode;
group?: string;
description?: string;
hideFromNavigation?: boolean;
nestedRoutes?: boolean;
roles?: string[];
}
Static Views
<RebaseAdmin
collections={collections}
views={[
{ slug: "dashboard", name: "Dashboard", icon: "LayoutDashboard", view: <Dashboard /> },
{ slug: "reports", name: "Reports", icon: "FileText", view: <Reports />, group: "Analytics" },
{ slug: "audit-log", name: "Audit Log", icon: "ScrollText", view: <AuditLog />, roles: ["admin"] },
]}
/>
Builder Function (Role-Aware)
Pass a function instead of an array to dynamically resolve views based on the current user:
<RebaseAdmin
collections={collections}
views={({ user, authController, data }) => [
{ slug: "dashboard", name: "Dashboard", icon: "LayoutDashboard", view: <Dashboard /> },
...(user?.roles?.includes("analyst")
? [{ slug: "reports", name: "Reports", icon: "FileText", view: <Reports /> }]
: []),
]}
/>
The builder receives { user, authController, data } and can return a Promise<AppView[]> for async resolution.
Plugin-Contributed Views
Plugins can also contribute views via the views property on RebasePlugin:
const myPlugin: RebasePlugin = {
key: "analytics",
views: [
{ slug: "analytics", name: "Analytics", icon: "BarChart3", view: <Analytics /> }
]
};
<RebaseAdmin plugins={[myPlugin]} />
All views (CMS, builder, plugin) are merged in order: CMS views → Studio dev views → Plugin views.
Role Filtering
The roles field provides declarative access control. When set, the view is excluded entirely (not just hidden from nav) if the user doesn't have at least one matching role:
{ slug: "admin-panel", name: "Admin", view: <AdminPanel />, roles: ["admin"] }
{ slug: "editor", name: "Editor", view: <Editor />, roles: ["admin", "editor"] }
{ slug: "dashboard", name: "Dashboard", view: <Dashboard /> }
IMPORTANT FOR AGENTS: The roles filter applies to ALL views — CMS views, builder-returned views, and plugin views. Use roles for simple role gates and the builder function for dynamic/async conditions. Both compose.
Navigation Grouping
Views participate in the same navigation group system as collections. Use the group property on the view, or control grouping centrally via navigationGroupMappings:
<RebaseAdmin
collections={collections}
views={[
{ slug: "dashboard", name: "Dashboard", view: <Dashboard />, group: "Analytics" },
{ slug: "reports", name: "Reports", view: <Reports />, group: "Analytics" },
]}
navigationGroupMappings={[
{ name: "Content", entries: ["posts", "pages"] },
{ name: "Analytics", entries: ["dashboard", "reports"] },
]}
/>
11. Where the Admin Lives — Mounting Under a URL Prefix
In the default scaffold the frontend is the admin panel: App.tsx renders <RebaseAuth> + <RebaseAdmin> + <RebaseShell> at the root, so the deployed URL shows the admin directly. There is no separate admin URL — in production one server serves both the API (/api/*) and this SPA.
When a project ships its own product app as the frontend, the admin is mounted under a prefix (commonly /admin) instead. Split the Vite entry by URL so each app is lazy-loaded and product visitors never download the admin bundle:
const isAdmin = window.location.pathname.startsWith("/admin");
const ProductApp = lazy(() => import("./App"));
const AdminApp = lazy(() => import("./AdminApp"));
if (isAdmin) {
const router = createBrowserRouter([{ path: "/admin/*", element: <AdminApp /> }]);
root.render(<RouterProvider router={router} />);
} else {
root.render(<BrowserRouter><ProductApp /></BrowserRouter>);
}
<RebaseAdmin collections={collections} basePath="/admin" />
IMPORTANT FOR AGENTS: Choose either a router basename="/admin" or <RebaseAdmin basePath="/admin"> — never both, or the prefix is applied twice. Without either, collection views hang on a spinner because URL⇄collection resolution never matches. <RebaseAdmin> requires a data router (createBrowserRouter) because it uses useBlocker.
12. Custom Field and Preview Components
To replace how a single property is edited or displayed, point at your component from the property's admin block. Field is the form editor; Preview is the read-only rendering used in table cells and reference previews.
body_parts: {
name: "Affected body parts",
type: "array",
of: { type: "string", name: "Part", enum: [] },
admin: {
Field: () => import("../../frontend/src/BodyPartsField"),
Preview: () => import("../../frontend/src/BodyPartsPreview")
}
}
The value is a lazy import thunk, and that is deliberate: a collection file is read by the server as well, so it must not import React. The thunk is type-checked at the call site and never called by the backend. The admin resolves it with React.lazy on first render. A direct component reference (Field: MyField) also works in a file the backend never loads.
The component itself is ordinary React, in frontend/:
import type { FieldProps } from "@rebasepro/admin";
import type { ArrayProperty } from "@rebasepro/types";
export default function BodyPartsField({ value, setValue, property, error, disabled }: FieldProps<ArrayProperty>) {
return null;
}
import type { PropertyPreviewProps } from "@rebasepro/admin";
import type { ArrayProperty } from "@rebasepro/types";
export default function BodyPartsPreview({ value, property, size }: PropertyPreviewProps<ArrayProperty>) {
return null;
}
Read the enum, labels and limits off the property argument rather than restating them, so the component cannot drift from the collection.
IMPORTANT FOR AGENTS: the module must have a default export — the resolver takes default from what the thunk resolves to, and a named-only export renders nothing. A component whose name begins with a capital and takes no props is treated as a component, not a loader, so Preview: () => null is a (useless) component while Preview: () => import("…") is a loader; the distinction is made on the dynamic import in the body, not the name.
Exported Components
The admin package exports the following components (from @rebasepro/admin):
| Component | Description |
|---|
RebaseAdmin | Declarative CMS config (collections, views, editor) — renders nothing |
RebaseShell | App shell (drawer, nav, routes, layout) — renders the actual UI |
CollectionPanel | Embed a collection view inside custom pages |
DataCollectionView | The collection view component |
EntityCustomView | Entity detail/edit view |
EntityPreview | Reference/relation preview chip |
EntityCard | Card representation of a entity |
SideDialogs | Side dialog container |
SideEntityProvider | Context provider for side entity controller |
EntitySelectionTable | Table for selecting entities |
Scaffold | Layout scaffold component |
AppBar | Top app bar |
Drawer / DefaultDrawer | Sidebar drawer |
RebaseAuthGate | Auth-gated wrapper |
RebaseNavigation | Navigation provider |
RebaseLayout | Layout wrapper |
RebaseRouteDefs | Route definitions |
Exported Hooks
| Hook | Description |
|---|
useSidePanel() | Open/close entity side panels |
useSideDialogsController() | Open/close generic side dialogs |
useUrlController() | Build URLs and navigate |
useNavigationStateController() | Access navigation state |
useCollectionRegistryController() | Look up collections by slug |
useBreadcrumbsController() | Read/set breadcrumbs |
useCMSContext() | Full CMS context (core + CMS controllers) |
useSelectionDialog() | Open entity selection dialog |
useSelectionController() | Multi-select controller |
useHistory() | Entity version history |
useApp() | App-level utilities |
Exported Utilities
| Utility | Description |
|---|
addInitialSlash(path) | Ensure path starts with / |
removeInitialSlash(path) | Strip leading / |
removeTrailingSlash(path) | Strip trailing / |
removeInitialAndTrailingSlashes(path) | Strip both |
getLastSegment(path) | Get last path segment |
getCollectionBySlugWithin(collections, slug) | Find collection in array |
mergeEntityActions(...) | Merge entity action arrays |
resolveEntityAction(...) | Resolve a entity action |
resolveEntityView(...) | Resolve a entity view |
isReferenceProperty(prop) | Check if property is a reference |
isRelationProperty(prop) | Check if property is a relation |
getIconForProperty(prop) | Get icon for a property type |
Built-in Entity Actions
import {
editEntityAction,
copyEntityAction,
deleteEntityAction,
resetPasswordAction
} from "@rebasepro/admin";
These are pre-built EntityAction objects that can be added to a collection's entityActions array.