Frontend Overview
Overview
Section titled “Overview”The Rebase frontend is a React framework that renders your admin panel. It reads your collection definitions and generates tables, forms, navigation, and routing automatically.
In the default scaffold, the admin panel is the frontend: it’s served at the root of your deployed URL. If you build your own product app instead, you can mount the admin under a prefix like /admin in the same deployment — see Changing the Base URL.
This is frontend/src/App.tsx as rebase init writes it — the whole admin
panel, four declarations inside one provider:
import React from "react";import { Rebase, RebaseAuth, useRebaseAuthController } from "@rebasepro/app";import { RebaseCMS, RebaseShell } from "@rebasepro/cms";import { RebaseStudio } from "@rebasepro/studio";import { createRebaseClient } from "@rebasepro/client";import { collections } from "virtual:rebase-collections";
const client = createRebaseClient({ baseUrl: import.meta.env.VITE_API_URL, auth: { authFlowMode: "cookie" }});
export function App() { const authController = useRebaseAuthController({ client });
return ( <Rebase client={client} authController={authController}> {/* Sign-in screen. Pass `loginView` to replace it. */} <RebaseAuth/> <RebaseCMS collections={collections}/> <RebaseStudio/> <RebaseShell title="My App"/> </Rebase> );}The first three render nothing: they register configuration into the
provider. <RebaseShell> is what draws — it reads that registry and builds the
navigation, routes and layout from it. So the order they appear in does not
matter, and adding a feature means adding a component, not rewiring a tree.
| Component | Package | Registers |
|---|---|---|
<RebaseAuth> |
@rebasepro/app |
the sign-in screen (loginView) |
<RebaseCMS> |
@rebasepro/cms |
collections, custom views, the home page, the collection editor |
<RebaseStudio> |
@rebasepro/studio |
the developer tools (SQL, RLS, logs, backups…) |
<RebaseShell> |
@rebasepro/cms |
nothing — it renders the admin from everything above |
Drop <RebaseStudio> and you have a content-only CMS; drop <RebaseCMS> and
you have the developer tools alone. To lay the shell out by hand instead, see
Advanced: manual layout.
The Rebase Provider
Section titled “The Rebase Provider”<Rebase> is the root provider that makes all Rebase functionality available to child components via context. It accepts:
All twenty-three of them, in full — the table used to list ten, and two of those were props the component never read:
| Prop | Description |
|---|---|
children |
The admin’s root components — <RebaseCMS>, <RebaseStudio>, <RebaseShell>. A render function is the manual-layout escape hatch. |
apiUrl |
Base URL of the backend API, made available to every hook via useApiConfig() |
dateTimeFormat |
How dates are printed. Defaults to MMMM dd, yyyy, HH:mm:ss |
locale |
Initial language of the admin, and the locale dates are formatted in — see Translations |
client |
RebaseClient instance: the default source for data, auth and storage |
dataSources |
Extra data sources, for collections that name one — see Multiple sources |
authController |
Authentication state and methods. Replaces the client.auth subscription outright |
storageSource |
The default storage source, overriding client.storage |
storageSources |
Named storage sources beyond the default |
databaseAdmin |
Administrative database operations (SQL, schema discovery). Only Studio needs it |
userConfigPersistence |
Local UI preferences — column widths, collapsed groups |
onAnalyticsEvent |
Called for every analytics event the admin emits |
entityLinkBuilder |
Returns a URL for the “open in your app” button on an entity form |
plugins |
Plugin instances — see Plugins |
slots |
Slot contributions declared directly, without a plugin |
propertyConfigs |
Custom field widgets, keyed by the name a property names in propertyConfig |
entityViews |
Global custom entity view tabs |
collectionViews |
Custom collection view modes, available to any collection by key |
entityActions |
Global entity actions |
effectiveRoleController |
Simulate a different role while dev mode is on |
modeController |
Since 0.25 Hand Rebase the host app’s light/dark mode; Rebase then never touches <html>, prefers-color-scheme or localStorage — see Styling |
translations |
Override or extend any UI string, keyed by locale — see Translations |
components |
Replace built-in components — see Component Overrides |
The navigation, URL and collection-registry controllers are not <Rebase>
props — they are built by the hooks below and consumed inside the admin tree
(<RebaseShell> wires them for you in the default scaffold).
Neither is the URL prefix. When the admin is mounted under a path, that belongs
on <RebaseCMS basePath="/admin">, which is what resolves URLs to collections —
and only when the router has no basename of its own. See
Changing the Base URL.
Two data shapes
Section titled “Two data shapes”There are two data layers, and they are not interchangeable. Passing one where the other is expected is a type error, so this is worth knowing before you wire a controller by hand.
| Shape | Where you get it | What a row looks like | |
|---|---|---|---|
| SDK | RebaseSdkData — flat rows |
client.data, and context.data in backend callbacks |
row.title |
| Admin | RebaseData — Entity view-model |
useData(), inside the <Rebase> tree |
entity.values.title |
The SDK layer is the public, symmetric surface: identical on the frontend client
and in backend callbacks. The Entity layer is the admin’s view-model — it adds
the id / path / values wrapper that the collection views and forms render
against. CollectionAccessor and FindResponse belong to it and are marked
@internal for that reason.
<Rebase> is the boundary between them: it takes your flat client.data and
wraps it with wrapAsEntityData() before providing it as the admin’s
RebaseData. You never call that yourself — you just take the shape you need
from the right place:
// Flat rows — anywhere, including outside React.const { data: posts } = await client.data.posts.find();posts[0].title;
// Entity view-model — inside the <Rebase> tree only.// `data.posts` also works at runtime; `collection()` is the typed accessor.const data = useData();const { data: entities } = await data.collection("posts").find();entities[0].values.title;One collection or one record in a page of your own
Section titled “One collection or one record in a page of your own”Since 0.25 On 0.24
CollectionPanel takes only a registered path and renders “Collection not
found” outside the full admin tree, and EntityFormPanel and
CollectionsProvider do not exist.
You do not need the whole admin to show one table. <CollectionPanel> takes
the collection as a value and works anywhere under <Rebase> and a React
Router router, with no <RebaseCMS>, <RebaseNavigation> or <RebaseShell>:
import { Rebase, useRebaseAuthController } from "@rebasepro/app";import { CollectionPanel } from "@rebasepro/cms";import { postsCollection } from "../config/collections/posts";// Plus BrowserRouter, or any other router, from "react-router".
export function App() { const authController = useRebaseAuthController({ client });
return ( <Rebase client={client} authController={authController}> <BrowserRouter> <MyPageLayout> <CollectionPanel collection={postsCollection}/> </MyPageLayout> </BrowserRouter> </Rebase> );}A record opens over your page, in a side panel or a dialog, and never by URL:
no admin route serves the page, so full_screen and split open as a side
panel, and useUrlController().routesMounted is false for your own
components there. Any router works: a plain <BrowserRouter>, a
<MemoryRouter>, or a data router. A router is still required. A data router
also lets a record with unsaved changes stop in-app navigation; under the
others only the browser’s leave-page prompt guards it. Wrap the panel in <RebaseAuthGate> if the page
should show Rebase’s sign-in first.
<EntityFormPanel> does the same for one record’s form. It loads the record
by entityId, saves it through <Rebase>’s data layer as the admin’s own
form does, and calls onSaved after every save. Omit entityId to create a
record. onClose is called when the form asks to be left, for example after
its record is deleted:
<EntityFormPanel collection={postsCollection} entityId={postId} onSaved={({ entity }) => console.log("saved", entity.id)} onClose={() => navigate("/posts")}/>Both bring their own CollectionsProvider when none is above them. That is
the step underneath, and you can mount it yourself to share one scope between
several panels, or to use the lower-level pieces (CollectionViewBinding,
useSidePanel) under it. Inside it, a panel can name its collection by path:
import { CollectionPanel, CollectionsProvider } from "@rebasepro/cms";
const collections = [postsCollection, authorsCollection];
<CollectionsProvider collections={collections}> <CollectionPanel path="posts"/> <CollectionPanel path="authors"/></CollectionsProvider>CollectionsProvider holds a registry of the collections you hand it, with
your plugins’ modifyCollection and injectCollections applied as in the
admin, plus the side panels and dialogs records open in, and the
unsaved-changes guard. Keep the collections array stable: a module constant,
or useMemo.
Advanced: manual layout
Section titled “Advanced: manual layout”Everything below replaces <RebaseShell>. You need it only when the stock
layout is in the way — a different chrome around the admin, a route tree of
your own, an app where the admin is one page among many. If you are not
replacing the layout, stop at Custom Views.
<RebaseShell> is sugar for four layers, and you can take them one at a time:
<Rebase client={client} authController={authController}> <RebaseCMS collections={collections}/> <RebaseStudio/>
{/* login screen until there is a user */} <RebaseAuthGate> {/* builds the navigation, URL and collection-registry controllers */} <RebaseNavigation> {/* the admin's routes, drawn inside the layout you pass */} <RebaseRouteDefs layout={<RebaseLayout title="My App"/>}/> </RebaseNavigation> </RebaseAuthGate></Rebase>The order is fixed: RebaseAuthGate → RebaseNavigation → RebaseRouteDefs → RebaseLayout. RebaseAuthGate shows the login view until there is a user, so
nothing below it renders for a signed-out visitor; RebaseNavigation builds
the navigation, URL and collection-registry controllers that RebaseRouteDefs
and every collection view read, so RebaseRouteDefs outside it throws.
Since 0.25 To embed a
collection view or a record form in a page of your own, without the
admin’s routes, the lighter CollectionsProvider builds those controllers
instead — see
One collection or one record in a page of your own.
Each layer is usable on its own. <RebaseAuthGate> alone gates your own app
behind Rebase’s login. Swap <RebaseLayout> for your own component to keep the
routing and lose the chrome; drop <RebaseRouteDefs> too and you are building
the routes yourself out of the components in
Scaffold Components.
Below that floor <Rebase> also accepts a render prop instead of children,
which hands you the context and the loading flag and leaves the entire tree to
you:
<Rebase client={rebaseClient} authController={authController}> {({ context, loading }) => ( <Scaffold> <AppBar/> <Drawer title="My App"/> <Outlet/> <SideDialogs/> </Scaffold> )}</Rebase>At that point nothing is wired for you: you build the controllers below by hand and render the routes yourself.
Controllers
Section titled “Controllers”Controllers are React hooks that configure specific aspects of the framework.
<RebaseNavigation> calls all of them for you — reach for these only inside a
render prop.
useBuildNavigationStateController
Section titled “useBuildNavigationStateController”The main controller that wires everything together:
Its data is the Entity-shaped RebaseData, so it comes from useData()
— not from rebaseClient.data, which is the flat-row SDK layer. <Rebase>
converts one to the other for you (see Two data shapes
below), so this hook must be called inside the <Rebase> tree.
const data = useData();
const navigationStateController = useBuildNavigationStateController({ collections: () => [...collections], // Collection definitions views: customViews, // Custom navigation views plugins, // Plugin instances authController, data, collectionRegistryController, urlController, adminMode: adminModeController.mode});useBuildCollectionRegistryController
Section titled “useBuildCollectionRegistryController”Manages how collections are resolved from URL paths:
const collectionRegistryController = useBuildCollectionRegistryController({ userConfigPersistence});useBuildUrlController
Section titled “useBuildUrlController”Configures URL generation:
const urlController = useBuildUrlController({ basePath: "/", baseCollectionPath: "/c", collectionRegistryController});useBuildModeController
Section titled “useBuildModeController”Manages the light/dark theme. While enabled it owns the page’s theme: it puts
the dark class on <html>, follows prefers-color-scheme until the user
picks a mode, and remembers that pick in localStorage.
const modeController = useBuildModeController();
modeController.mode; // "light" | "dark"modeController.setMode("dark"); // "light" | "dark" | "system" — "system" follows the OS again<Rebase> builds one of these itself.
Since 0.25 When the
component using it unmounts, it removes the dark class it added; a dark
class the page already had stays. Build your own
above <Rebase> only when components outside it need the mode too, and hand
it over with the modeController prop, so there is one controller
and not two. useBuildModeController({ enabled: false }) builds an inert
controller: it keeps mode in React state and touches no class, media query
or localStorage. When your app already owns the theme you do not need this
hook at all: pass your own controller as modeController.
useBuildAdminModeController
Section titled “useBuildAdminModeController”Toggles between Studio and Content modes:
const adminModeController = useBuildAdminModeController();// Provides: adminModeController.mode ("cms" | "studio")Scaffold Components
Section titled “Scaffold Components”| Component | Description |
|---|---|
<Scaffold> |
Main layout container with responsive sidebar |
<AppBar> |
Top navigation bar with search, mode toggle, user menu |
<Drawer> |
Side navigation with collection list and view links |
<SideDialogs> |
Container for side panel entity editors |
<RebaseRoutes> |
Route container that integrates with React Router |
<RebaseRoute> |
Handles collection routes (/c/*) |
<ContentHomePage> |
Default home page showing collection cards |
<StudioHomePage> |
Studio mode home page with developer tools |
Custom Views
Section titled “Custom Views”Add top-level navigation views for dashboards, tools, or custom pages. An
AppView is a flat object — everything below sits at the top level, there is no
nested admin block:
import type { AppView } from "@rebasepro/cms-types";
const views: AppView[] = [ { slug: "dashboard", name: "Dashboard", icon: "LayoutDashboard", view: <MyDashboard/> }, { slug: "settings", name: "App Settings", icon: "Settings", group: "Admin", // Register `settings/*` too, so the view can route inside itself. nestedRoutes: true, // Reachable by URL, but not listed in the drawer. hideFromNavigation: true, view: <AppSettings/> }];Hand them to <RebaseCMS>, next to your collections — that is the component
that registers navigation:
<RebaseCMS collections={collections} views={views}/>| Field | |
|---|---|
slug |
the path it is reached at, under the admin root |
name |
the label in the drawer and on the home page |
view |
the element to render, or a ComponentType to render it lazily |
icon |
a Lucide icon name, e.g. "ShoppingCart" — or any node |
group |
groups views together in the drawer; "Admin" and "Settings" sink to the bottom |
pinToBottom |
sinks the group to the bottom under any name — prefer it over the two magic strings |
nestedRoutes |
also registers slug/*, for a view with routes of its own |
hideFromNavigation |
keeps the route, drops the nav entry |
roles |
only users holding one of these roles see the view, or can reach it |
description |
Markdown, shown on the home-page card |
To put a view under Studio instead of the CMS, pass it to
<RebaseStudio devViews>.
Styling
Section titled “Styling”Rebase uses Tailwind CSS v4 and supports light/dark modes. Customize via:
- CSS custom properties — Override design tokens
<Rebase modeController>— Hand Rebase your app’s light/dark mode- Tailwind config — Standard Tailwind customization
/* Override design tokens */:root { --font-sans: "Instrument Sans", sans-serif; --font-headers: "Instrument Sans", sans-serif; --font-mono: "JetBrains Mono", monospace;}Light and dark mode
Section titled “Light and dark mode”By default <Rebase> owns the page’s theme: it puts the dark class on
<html>, follows prefers-color-scheme until the user picks a mode in the
admin, and remembers that pick in localStorage.
Since 0.25 When <Rebase>
unmounts, it removes the dark class it added; a class the page already had
stays. On 0.24 the class stays behind.
Since 0.25 When your app
already has a theme, pass it as modeController. Rebase reads mode from it,
calls its setMode from its own theme toggle, and never touches <html>,
prefers-color-scheme or localStorage:
import { useEffect, useMemo, useState } from "react";import { Rebase, type ModeController } from "@rebasepro/app";
export function App() { const [mode, setMode] = useState<"light" | "dark">("light"); const modeController = useMemo<ModeController>(() => ({ mode, setMode: (next) => setMode(next === "dark" ? "dark" : "light") }), [mode]);
// Your app applies the mode, as it does for the rest of the page. useEffect(() => { document.documentElement.classList.toggle("dark", mode === "dark"); }, [mode]);
return ( <Rebase client={client} modeController={modeController}> <MyAdmin/> </Rebase> );}The scaffold’s CSS styles dark mode under any element with the dark class
(@custom-variant dark (&:where(.dark, .dark *))). Side panels, dialogs and
menus render in a portal on <body>, so put the class on <html> or <body>
rather than on a wrapper around <Rebase>.
Wrapping <Rebase> in a ModeControllerProvider does not do this:
<Rebase> provides a controller of its own inside it. On 0.24 the prop does
not exist, and there is no way to hand <Rebase> your app’s mode.
Next Steps
Section titled “Next Steps”- Custom Fields — Build custom form fields
- Entity Views — Add tabs to entity editors
- View Modes — List, Table, Cards, Kanban
- Translations — Change any string, or add a language
- Plugins — Extend the framework