An admin panel
A back-office asks four questions: which subjects exist, what a row shows, what a form takes, and
which actions are allowed. The app's identity card — what rpc.discover answers — already says all
four, so @fougere/admin generates nothing and declares nothing: it reads the card when it loads and
renders it.
npm install @fougere/admin react-admin
pnpm add @fougere/admin react-admin
bun add @fougere/admin react-admin
yarn add @fougere/admin react-admin
// app/admin.tsx — the whole file
import { FougereAdmin } from '@fougere/admin/react';
export default () => <FougereAdmin />;
Every entity, with nothing more to write. A new class Invoice extends entity({…}) appears in the
menu at the next load, because the card now lists its facade.
What each screen is read from
| Screen | Read from | Function |
|---|---|---|
| The menu | card.fronds[].facades[] | a facade whose list answers no entity is not furniture |
| A list's columns | Visibility.output + the shape | tableColumnsOf |
| A form's inputs | Visibility.input + the lifecycle axis | formFieldsOf |
| Its bounds | shape → minLength, max, pattern | field.attrs |
| A reference | the relation role, then the target's first text() | field.reference, searched twenty at a time |
| A row's identity | the primary role | FieldSet.of(fields).primary |
| The buttons | facade.ops | no create op, no create button |
| A refusal | VALIDATION_FAILED | errorsByField, per field |
| Topology | rpc.topology | the one screen the card does not answer |
tableColumnsOf reads what may leave, formFieldsOf what a client may write — the same pair
a form uses. The panel adds no reading of its own.
The gradient reaches it
The panel talks to the call endpoint and knows nothing else, so this line changes nothing for it:
// fougere.config.ts
fronds: { billing: { remote: 'https://billing.internal' } }
The invoice facade moves to another process, the card still lists it, and the same build renders
the same screens.
Facets — declared, never guessed
Some things a screen wants are not in the entity. oneOf('draft', 'published') says the set is
closed; it never says which value means published. You say it:
import { defineAdminExtension } from '@fougere/admin';
defineAdminExtension({
resource: 'post',
facets: {
editorial: {
title: 'title',
state: { field: 'status', draft: ['draft'], published: ['published'] },
},
},
})
Nothing is guessed from a field's name: an entity writing titre or brouillon would lose its facet
without a word. With no facet declared, the table read from the card is still the whole back-office.
Extensions are deltas
An extension names only what it changes, so a field added to the entity tomorrow still appears:
defineAdminExtension({
resource: 'post',
label: 'Articles',
fields: { internalNote: { hidden: true }, title: { label: 'Headline' } },
operations: { publish: { confirm: 'Publish this article?' } },
})
For anything finer, one renderer replaces one field, and every React Admin prop still
works — theme, layout, authProvider, i18nProvider, dashboard:
<FougereAdmin
renderers={{ fields: { 'user.role': ({ column }) => <ChipField source={column.name} /> } }}
resourceComponents={{ post: { list: MyPostList } }}
/>
It speaks no language
Every visible sentence goes through a key: fougere.admin.* for the panel's own widgets,
entity.field for a field, with the field's name when no key answers. An untranslated admin is
English, and an i18nProvider filling those keys is the whole translation.
polyglotI18nProvider(() => ({
...frenchMessages,
fougere: { admin: { overview: { title: "Vue d'ensemble" } } },
post: { title: 'Titre', status: 'Statut' },
}), 'fr')
The one page that is not a facade
Every screen above renders a facade. Topology renders the app:
catalog here 1 entity · 1 facade → blog (12)
blog elsewhere its shape is published by the process that owns it ← catalog (12)
search declared, unheard declared at search.internal:4500 — nothing has answered from it
It is read from rpc.topology, not from the card: which fronds run in the process the panel talks
to, which answered from elsewhere, and the calls seen between them. The last line comes from what
the config declares: a frond that never answered would otherwise be missing, and a system with a
node down would look like a smaller healthy one.
rpc.topology is served by @fougere/observability. Pointed at an app without
it, the page says which two lines to add.
What it will not do
- Remove a resource or an operation. Hiding it in the browser while the server still serves it
would look like a permission and enforce nothing. What one audience may call is a
surface: point the panel at
/_fougere/call/adminand the card it reads is the restricted one. A field may be hidden: that changes nothing about what is reachable. - Write around the checks. It runs in a browser and reaches only the call endpoint, so every read and write it makes goes through validation, presenters and the links around calls.
- Aggregate. The only aggregate rows answer is
count, so the dashboard counts. "Revenue over thirty days" is an operation you write in a repository; the panel shows its result. - Own your pages. It is a panel for whoever administers the data. The application's own screens stay yours, built on queries and commands.