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
// 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

ScreenRead fromFunction
The menucard.fronds[].facades[]a facade whose list answers no entity is not furniture
A list's columnsVisibility.output + the shapetableColumnsOf
A form's inputsVisibility.input + the lifecycle axisformFieldsOf
Its boundsshape → minLength, max, patternfield.attrs
A referencethe relation role, then the target's first text()field.reference, searched twenty at a time
A row's identitythe primary roleFieldSet.of(fields).primary
The buttonsfacade.opsno create op, no create button
A refusalVALIDATION_FAILEDerrorsByField, per field
Topologyrpc.topologythe 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/admin and 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.
Built with Fougere and Nuxt, hosted on GitHub Pages.