Admin
Every route under /admin/* is gated by a server-admin auth check.
Auth guard
Section titled “Auth guard”Separate from the standard route() wrapper's auth -- routes/admin/index.js implements its own middleware:
- Requires
Authorization: Bearer <jwt>orAuthorization: Token <jwt>. - Verifies RS256 against
settings.publicKey, issuerhttps://<domain>. - Calls
isServerAdmin(userId).
| Status | Meaning |
|---|---|
| 401 | missing or invalid token |
| 403 | valid token, but the user isn't a server admin |
Browser-navigation guard: if the frontend is enabled and the request has no Authorization header and prefers html, it's deferred to the SPA instead of returning JSON -- same pattern as elsewhere, see Overview.
The shared CRUD shape
Section titled “The shared CRUD shape”Most admin resource groups (users, posts, circles, groups, pages, recommendations, sections) follow the same pattern:
GET /-- list. Admin can see soft-deleted items via?deleted=true, or both deleted and live via?deleted=include. UsuallymakeCollection, sometimes manual.GET /:id-- detail.POST /-- create (for content types, always server-owned).PATCH /:id-- update.DELETE /:id-- soft-delete by default.?fullDelete=truehard-deletes.POST /:id/restore-- un-delete a soft-deleted item.
Only the meaningful differences per group are called out below -- assume the shape above unless noted otherwise.
/admin/users
Section titled “/admin/users”GET /, GET /:id only -- no create/update, since users self-register. DELETE /:id soft-deletes (deletedAt, active: false), or hard-deletes with ?fullDelete=true. POST /:id/restore reactivates.
/admin/posts
Section titled “/admin/posts”Create/update are scoped to server-owned announcement posts only.
?visibility=all on the list includes circle-addressed (private) posts -- the default list only shows @public/@server posts.
/admin/circles
Section titled “/admin/circles”Same server-owned-only edit restriction as posts. ?server=true filters the list to server-owned circles; ?type= overrides the default type: "Circle" filter.
/admin/groups
Section titled “/admin/groups”Same server-owned-only edit restriction as posts. ?rsvpPolicy= filter on the list.
/admin/pages
Section titled “/admin/pages”/admin/flagged
Section titled “/admin/flagged”The moderation queue over the Flag model.
GET /--?status=(default"open"),?targetType=,?actorId=GET /:idPATCH /:id-- body{ "status": "resolved" | "dismissed", "notes": "..." }
No create/delete here -- flags are user-generated via POST /outbox { type: "Flag" } (see Activities), out of scope for this admin router.
/admin/invites
Section titled “/admin/invites”Mounted as flat routes directly on routes/admin/index.js itself, not a sub-router:
POST /invites-- body:({ "type": "individual", "email": "...", "maxRedemptions": 1, "expiresAt": "...", "note": "...", "welcomeMessage": "..." }emailrequired fortype: "individual".) Individual invites trigger an email send -- best-effort, a failure is logged but not fatal to the request.GET /invitesGET /invites/:idDELETE /invites/:id-- deactivates (soft), doesn't remove the row.
/admin/settings
Section titled “/admin/settings”GET /-- lists all Settings docs. Values are redacted to"[redacted]"forto: "@private"settings or the specificname: "privateKey"setting.PATCH /-- bulk update. Body is the settings object directly, not wrapped:{ "settingName": value, ... }. Any key whose doc hascanEdit: "@private"orui.type: "redacted"is rejected wholesale -- the entire request fails with403listing the blocked keys, not a partial-success.PATCH /:name-- single-setting update, body{ "value": ... }.
Two special-cased behaviors:
- The
rulessetting is normalized throughnormalizeRuleson write. - Settings named in
HTML_FIELDS_BY_SETTING(currently justprofile.description) get HTML-sanitized on write.
/admin/system
Section titled “/admin/system”A genuine grab-bag -- several unrelated concerns share this router:
Diagnostics -- GET / returns:
{ "db": { /* db.stats() */ }, "counts": { "users": 0, "posts": 0, "groups": 0, "circles": 0, "pages": 0, "replies": 0, "reacts": 0, "activities": 0, "openFlags": 0, "activeInvites": 0 }, "disk": { /* statfs on cwd, best-effort */ }, "process": { /* node/process/memory stats */ }}Admin/mod membership -- also on this router, despite not really being "system" diagnostics:
GET/POST /system/admins,DELETE /system/admins/:userIdGET/POST /system/mods,DELETE /system/mods/:userId
These manage the server's admin/mod circles directly -- deliberately bypassing the normal ActivityPub Add/Remove activity pipeline, since these circles are server-owned rather than user-owned and don't need federation/audit-trail semantics applied.
Logs -- GET /system/logs?tail=&level= -- tails the app log file, capped at 2000 lines.
/admin/backup
Section titled “/admin/backup”Async backup/restore job queue, backed by BackupJob plus S3/MinIO archive storage, that also archives S3-stored files (not just Mongo documents) without blocking the event loop. Requires a separate worker process (workers/backup.js) to actually run jobs -- see the note below.
POST /-- queues a backup job.409if one's already running.GET /-- lists the last 20 jobs.GET /:id-- job status.GET /:id/download-- streams the completed archive.409ifstatus !== "done".DELETE /:id-- deletes the job record and its S3 archive.POST /restore--multipart/form-data, fieldarchive, up to 512MB. Uploads to a temp S3 key and queues a restore job.409if a job is already in flight,400if no file was sent.
/admin/activities
Section titled “/admin/activities”GET / only -- makeCollection over the raw Activity audit log. ?type=, ?actorId=, ?objectType=.
/admin/recommendations + /admin/sections
Section titled “/admin/recommendations + /admin/sections”The curation CRUD backing the public Discover feed.
/admin/recommendations:
POST /-- body{ "ref": "...", "section": "...", "note": "...", "order": 0 }.- Validates
refresolves to a curatable type:Post | Circle | Group | Bookmark | Page. - Users cannot be recommended --
400if you try. - Validates the target exists and isn't private-tier --
400if private. - Snapshots the target's tier into
visibilityat add-time -- but note (per the public endpoint's docs) the live target is still what's checked at read time; this snapshot isn't used for enforcement, just bookkeeping.
- Validates
PATCH /:id-- allowsnote,order,active,section.
/admin/sections:
- CRUD for the Discover shelves themselves:
name,summary,order,to,active. DELETE /:id?fullDelete=true-- cascade-deletes that section's recommendations too, not just the section row.