Plugin registry (Xolize admin)
Business view
Before any clinic can see a plugin in their Plugin Hub, someone at Xolize has to create and publish its listing. There is no self-serve developer portal in this release — a partner developer emails Xolize (per docs/plugin-platform/01-getting-started.md, devrel@dentolize.com), and a Xolize staff member enters the listing by hand into an internal admin tool.
This tool is deliberately not discoverable: it lives behind a private, unguessable URL rather than a normal /admin path, with its own separate login — a Xolize staff account is not the same thing as a clinic account, and the two logins don't overlap.
From this tool, a Xolize reviewer can:
- Create a new listing or edit an existing one — name, description (English and Arabic), icon, screenshots, categories, developer contact info, and the exact list of data scopes and webhook events the plugin is requesting.
- Author and validate the plugin's settings-form schema, with a structural preview (though not a pixel-accurate one — the real rendered form only exists in the clinic-facing Plugin Hub).
- Leave internal review notes on a listing.
- Publish a listing, making it installable by every clinic, or suspend one, which immediately blocks that plugin's API access platform-wide.
This is the exact form a plugin author fills in, and it is literally the same content the clinic sees on the consent screen — there's no separate "marketing description" vs. "technical scopes" split; whatever a reviewer types into the Permissions section on this screen is what the clinic reads when deciding whether to trust the plugin.
Technical view
Access and routing
Everything sits behind an obscured, non-guessable path rather than a public /admin prefix — ADMIN_LINK = 'ZROYKuKEVCvQykPlS4kP' (packages/clinic-web/src/variables.js:44) — reached at /${ADMIN_LINK}/plugins, /${ADMIN_LINK}/plugin/new, and /${ADMIN_LINK}/plugin/:id (AdminDashboardRouter.js:167-169). This is a completely separate auth system from clinic login: a distinct useAdmin() session context, a distinct login route (/${ADMIN_LINK}/auth/login), and a distinct guard (AdminDashboardRouter.js:129) redirecting unauthenticated admin sessions there. The sidebar entry (AdminSidebar.js:54) reads "Plugins" with an ApiOutlined icon, last item in the main admin menu.
(This walkthrough's live sandbox capture of this screen was blocked — the admin login route entered a load loop rather than rendering a form. The description below is verified directly from source, not from a live capture; see the note in Walkthrough.)
List screen (AdminPlugins.js)
A platform-wide table (no company/clinic scoping — this queries the whole registry) with search-by-name/slug/developer, a status filter, and columns Plugin / Slug / Status / Version / Developer / Categories / Updated. Status is rendered as a colored tag: DRAFT grey, IN_REVIEW gold, PUBLISHED green, SUSPENDED red, DEPRECATED purple (pluginAdminConstants.js).
Row actions:
- Edit — always available, →
/plugin/:id. - Publish — only shown when status is
DRAFT,IN_REVIEW, orSUSPENDED(PUBLISHABLE_STATUSES); confirms with "It will become visible and installable by all clinics." - Suspend — only shown when status is
PUBLISHED(SUSPENDABLE_STATUSES); confirms with "Clinics will no longer be able to install it and its API access will be blocked."
A workflow detail worth flagging: neither Popconfirm here collects a reason. To record why a plugin was suspended, a reviewer has to separately open the Edit screen, fill the Review Notes field, and save — a two-step, easy-to-miss sequence if the intent is to leave an audit trail alongside the suspension.
Status machine as actually reachable from this UI: {DRAFT, IN_REVIEW, SUSPENDED} → PUBLISHED and PUBLISHED → SUSPENDED. Nothing in this admin app transitions a listing into IN_REVIEW or DEPRECATED — there's no "submit for review" or "deprecate" button anywhere; those statuses exist as data values (and IN_REVIEW/DEPRECATED are listed as filter options) without a UI path that produces them in this PR.
Edit / create screen (AdminPlugin.js)
One screen serves both create (/plugin/new) and edit (/plugin/:id) — edit mode is detected purely from the presence of a route :id; there's no dedicated read-only detail view. Cards, top to bottom:
- Listing — slug (immutable after creation, pattern
^[a-z][a-z0-9-]{2,50}$), name (EN required, AR optional/RTL), description (EN required, AR optional), icon URL, free-form category tags, version (plain text, no semver enforcement). - Developer — name, URL, support email, privacy policy URL (the last two required).
- Permissions — multi-select scopes and multi-select webhook events, each rendered as
"<key> — <description>". The card's own subtitle states plainly: "Shown on the clinic consent screen at install time." Notably, there is no field here foroptionalScopes— the schema supports per-scope optional consent (see Scopes & consent), but this admin form only lets a reviewer set the singlerequestedScopeslist. - Screenshots — URL-only in v1 (no file upload); dynamic list of URL inputs.
- Changelog — dynamic list of
{version, date, notes, notesAr}entries; date is free text, not a real date picker. - Settings Schema — a monospace JSON textarea plus a Validate button. Validation runs
validateSettingsSchema(packages/clinic-web/src/components/admin/plugins/validateSettingsSchema.js) — explicitly documented in its header comment as a manually-synced local copy of the canonicalpackages/common/src/pluginSettingsSchema/validateSchema.ts, kept in sync by hand because this bundle can't import the canary TypeScript package directly. On success it renders a read-only structural tree (sections → fields → type/required/visibleIf tags), not a live rendering of the actual form — that only exists in the clinic-facing Plugin Hub. - Review (edit mode only) — free-text internal review notes, saved together with the rest of the form on submit; there's no separate approve/reject action distinct from Publish/Suspend on the list screen.
Submission is all-or-nothing: an invalid settings schema blocks the whole save, not just that section.
Known drift risk
Three files in the admin app are explicitly documented in their own header comments as hand-maintained mirrors of a canonical source elsewhere, with correctness ultimately guaranteed by server-side re-validation rather than by the mirrors themselves: validateSettingsSchema.js (mirrors packages/common/src/pluginSettingsSchema/validateSchema.ts), pluginAdminConstants.js (mirrors packages/server/src/plugins/auth/scopes.js and .../webhooks/events.js). If a scope or event is added to the server catalog and this admin file isn't updated in the same change, a reviewer simply won't see the new option in the multi-select — worth a smoke-test item (see For Quality).