This page adds a section to the admin app, the operator area on ADMIN_DOMAIN. Every feature of
the product has its counterpart there, and that counterpart is a module.
You need:
- The feature built (the aggregate reference).
- The rule it follows: "The admin panel stays in sync" in
AGENTS.md.
A module is one section of the icon rail: its entry, its pages, optional tabs on a person's page, and the API routes those pages call.
Two registries list the same ids in the same order, the rail's order. A test on each side holds
them to it: apps/admin/src/modules/registry.test.ts and apps/api/routes/admin/+routes.test.ts.
| Id | Pages | API under /api/admin |
|---|---|---|
dashboard |
/ |
GET /stats |
stats |
/stats |
GET /website-stats |
users |
/users, /users/:id |
/users/* |
groups |
/groups, /groups/:id |
/groups/* |
accounts |
/accounts |
GET /accounts |
categories |
/categories |
GET /categories |
billing |
/billing, /billing/:groupId |
/billing/* |
newsletter |
/newsletter |
/newsletter/* |
blog |
/blog, /blog/new, /blog/:id, /blog/:id/sharing, /blog/channels and /:id |
/blog/* |
contact-links |
/contact-links, /contact-links/new, /contact-links/:id |
/contact-links* |
catalog |
/catalog, /catalog/:id, /catalog/categories and /:id |
/catalog* |
audit |
/audit |
/audit* |
jobs |
/jobs, /jobs/:id |
/jobs* |
rates |
/rates |
GET /rates |
system |
/system |
/system* |
api-tokens |
/api-tokens |
/api-tokens* |
The api-tokens module also adds three tabs to a person's page: API tokens, Devices and
Notifications. It answers them under /api/admin/users/:id/.
Optional modules. An optional module is on only when the product turns it on. Its API module
has enabled(context), and its admin module has optional: true. When it is off, its routes are
not mounted and GET /api/admin/modules leaves it out. The catalog is the one optional module
(CATALOG=true, catalog.md).
| Where | What it holds |
|---|---|
apps/admin/src/modules/<id>/module.tsx |
The AdminModule: id, nav, routes and optional personTabs |
apps/admin/src/modules/<id>/state.ts |
State the module's pages share, over context.request, with a test |
libs/ui/admin-<id>-screen.tsx |
The screens: props in, callbacks out |
apps/api/routes/admin/<id>.ts |
The AdminApiModule: id and routes(context), a Hono app |
libs/server/<area>/admin-<id>.ts |
The SQL, when the module reads anything |
nav holds the rail entry's name, first page and icon.
Each page is a view that gets the AdminContext:
| Field | What it is |
|---|---|
request |
The API on the app's own origin. A 401 signs the app out |
toasts |
The app's toasts |
operator |
The signed-in operator |
navigate |
The router's navigate |
personTabs |
Every module's tabs for a person's page |
A view keeps its own state. The users module keeps one store per context, so a new session starts empty.
A person tab's render({ context, userId }) mounts only while the tab is open, so it may load its
own data then. Its id must differ from the page's own tabs (ADMIN_USER_TABS) and from every
other module's.
- Write the files above.
apps/admin/src/modules/coming-soon.tsxholds a placeholder page while the views are not built. - Add one import and one line to
ADMIN_MODULESinapps/admin/src/modules/registry.ts. - Add the same line, in the same place, to
ADMIN_API_MODULESinapps/api/routes/admin/registry.ts. - Test the API routes.
- Add the module's paths to
ROUTESinapps/api/routes/admin/+routes.test.ts. A loop over it checks that anyone below operator gets 404.
createAdminRoute (apps/api/routes/admin/+routes.ts) runs these checks before any module's
route:
- A signed-in session, with its second factor when it has one.
- The operator role or above. Anyone below gets 404, as if the area did not exist.
- The mutation guards.
A module cannot opt out: its routes are mounted after those checks. A path no module answers is 404 too.
A module answers counts and account facts only, never a note's text or any other aggregate's content.
One exception helps the operator remove abusive content. A group's Files tab shows each file's name
and the title of its note. The operator can open or download the file through the admin API, since
the admin host's img-src 'self' cannot load the file store.
With the notes off (NOTES=false), the group's file and overview widget routes are not mounted
and answer 404, since every file sits on a note and every widget counts notes.
Route under /api/admin/groups/:id/files/:fileId |
Audit row |
|---|---|
/open |
yes |
/download |
yes |
/thumbnail |
no |
The thumbnail route serves the whole original image; the 40 px tile only scales it down. So an
operator who opens the thumbnail's address sees the full image without an audit row. Thumbnails
spend a per-operator budget of their own, ten times the normal one (THUMBNAIL_LIMIT_FACTOR), so a
page of 25 tiles leaves the operator's other admin calls alone.