Skip to content

How it works

Two clocks. Almost every surprise in this library is mixing them up.

Mount time

await mount(app, admin) is async because this is when the schema is read.

  1. register("User") only stored intent. It did not talk to the database.
  2. mount calls adapter.introspect(). Prisma compiles schema.prisma (DMMF). TypeORM reads an initialized DataSource; MikroORM reads the initialized ORM's live metadata.
  3. Every registered name is checked against the real models.
  4. Field overrides, listDisplay, and searchFields are validated.
  5. Missing config is filled from that metadata (display field, columns, filters).
  6. Express gets a router at basePath (default /admin):
    • GET /admin/api/schema
    • CRUD and actions under /admin/api/:model
    • the pre-built UI for every other path

If you register("Users") and the model is User, mount throws with the list of real names. Fail at boot, not on the first click.

That is why mount must be awaited, and why every register must happen before mount.

Request time

The browser loads one HTML page. React reads the URL. Data comes from the API. There is no new HTML document per screen.

GET  /admin/api/schema
GET  /admin/api/users?page=1
GET  /admin/api/users/:id
POST /admin/api/users
PUT  /admin/api/users/:id
DELETE /admin/api/users/:id
POST /admin/api/users/actions/:action

Every /admin/api/* call runs this pipeline:

getCurrentUser → permission → scope → validate → adapter.resource() → audit?
StepWhat it decides
getCurrentUserWho is this? null → 401
PermissionMay this role list / view / create / update / delete / run the action?
scope()Which rows? Combined with the id so guessing another tenant's id is 404
ValidateOnly known, visible, writable fields. No nested writes.
AdapterPanelJS list/get/create/update/delete. Prisma, TypeORM, or MikroORM translates that into its own query calls.
audit.writeOptional. After success only. No field values.

The UI never talks to the ORM. It fetches /admin/api/schema once and renders lists and forms from that JSON.

Why /admin/posts/123 still works

There is no posts/123.html. Express serves index.html for any non-API path. React Router (basename /admin) shows the detail view. API routes are mounted first so they always win.

What the schema endpoint is

GET /admin/api/schema is the brain of the UI. It returns registered models, visible fields, list columns, filters, and this admin's permissions as booleans.

It is behind the same auth middleware as CRUD. It exposes field names, enums, and relations. Do not serve it anonymously.

The type the rest of the system uses

After mount, each model is a FullRegisteredModel:

  • meta — from the adapter (User, users, fields, id)
  • resolved — defaults filled in (listDisplay, perPage, …)
  • raw — your functions (scope, hooks, actions). Functions are not sent to the browser.

The router reads all three. The UI only sees a JSON slice of meta + resolved.

Released under the MIT License.