This repository is a template: the code under backend/mpt_extension_python_template/ and
frontend/src/ is a set of small, working examples of the extension building
blocks. This document is a guided tour of those examples and where to find them.
It describes what this repository demonstrates, not the SDK APIs themselves. For the library APIs, see the SDK usage guides linked from README.md.
backend/docs/postman_collection.json
is a ready-made Postman collection for a locally running backend (default
base_url http://localhost:8080, the port exposed by make run-local). Auth
is wired automatically through a collection-level pre-request script. It contains
sample requests for the example routes below — /health, the order and agreement
events, and the agreement API (including list pagination, get, not-found, and
sync) — so you can exercise the template without writing requests by hand.
backend/mpt_extension_python_template/app.py creates the
ExtensionApp and registers every example router:
events_orders_router— order event handlingevents_agreements_router— agreement event handlingapi_agreements_router— agreement API endpointsplug_portal_router— top-level portal plugs (thelearn-extensionsnavigation group with the examples app and guide nested under it, plus theaddshowcase)plug_modals_router— socketless modal plugs (dialogandwizard) opened by id from the examples "Modals" viewplug_agreements_router— agreement plugs (the agreement tab example plus the agreementsaddshowcase)plug_orders_router/plug_subscriptions_router/plug_assets_router/plug_accounts_router— per-entityaddshowcase plugs
Each router below is an independent example and can be read on its own.
backend/mpt_extension_python_template/routers/api/agreement.py
exposes an APIRouter under /api/v2/agreements:
| Route | Name | Demonstrates |
|---|---|---|
GET / |
agreements-list |
Paginated reads with ctx.request.pagination and PaginatedResult / APIResponse.paginated |
GET /{agreement_id} |
agreements-get |
Reading a single agreement via ctx.mpt_api_service and returning APIResponse.ok |
POST /{agreement_id}/sync |
agreements-sync |
A write-style action that re-reads current Marketplace data |
These handlers read Marketplace data through ctx.mpt_api_service.agreements.
The sync route is the backend half of the frontend "Sync now" example (see
Plugs).
The template shows the two event styles supported by the SDK:
- Task event —
routers/events/order.pyregisters@orders_router.task("/purchase")forplatform.commerce.order.status_changed, filtered by aconditionon the configuredproduct.ids. It receives a standardOrderContextand runs thePurchasePipeline. - Non-task event with a custom context —
routers/events/agreement.pyregisters@agreements_router.event("/complete")forplatform.commerce.agreement.status_changed, with a richer condition (...,eq(status,Active)) and acontext_adapter_type=EventAgreementContext. It runs theCompleteAgreementPipeline.
Both conditions are built from get_extension_settings().product_ids, showing
how event registration uses extension settings.
Event handlers delegate to small pipelines, demonstrating the pipeline/step composition pattern:
flows/pipelines/orders/purchase.py(PurchasePipeline) →flows/steps/log_order.py(LogOrderStep, logs the order id).flows/pipelines/agreements/complete.py(CompleteAgreementPipeline) →flows/steps/log_agreement.py(LogAgreementStep, logs the agreement id and the custom context field).
A pipeline is a BasePipeline whose steps property returns an ordered list of
BaseSteps. The steps here only log, on purpose — they are the place real
fulfillment logic would go.
backend/mpt_extension_python_template/context/agreement.py
defines EventAgreementContext, an example context adapter that extends the SDK
AgreementContext with an extra mock_field and overrides from_context. It is
wired into the agreement event (context_adapter_type=...) and consumed by
LogAgreementStep, showing how to carry extension-specific data through a
pipeline.
backend/mpt_extension_python_template/routers/plugs/agreements.py
declares a Marketplace Portal plug through a PlugRouter. The plug points
at a frontend bundle via href and binds to a Portal socket. The frontend
module name matches the bundle path one-to-one:
| Plug id / socket | Frontend module | Demonstrates |
|---|---|---|
agreements-agreement — portal.commerce.agreements.agreement |
modules/agreements-agreement/ |
A full agreement tab: sync status, useAgreementSync, status chips, and a "Sync now" action calling the sync API |
backend/mpt_extension_python_template/routers/plugs/modals.py
declares two ModalPlugs. A modal plug has no socket: the platform never
renders it as a page action, and it exists only to be opened programmatically
by id via useMPTModal().open('<plug-id>'). The examples app "Modals" view
(modules/examples/views/Modals.tsx)
opens both plugs by id, passes a context payload to them, and displays the
result each modal reports back through close(data) (delivered to the opener's
onClose callback):
| Plug id | Frontend module | Demonstrates |
|---|---|---|
dialog |
modules/dialog/ |
A confirmation dialog that renders the opener's question from useMPTContext() and returns { confirmed: true/false } |
wizard |
modules/wizard/ |
A multi-step Wizard that echoes the opener context and returns { completed: true/false } |
The result objects above are what these modals pass to close(data). When a
modal is dismissed without an explicit close — for example through the
platform's modal chrome — onClose receives undefined, so openers must guard
against an undefined result before reading fields like confirmed or
completed.
Each module's index.tsx follows the same entry-point pattern: import the
safe-storage shim, call setup() from @mpt-extension/sdk, and mount the
App with createRoot. The shared building blocks the agreement tab reuses
(useAgreement, useAgreementId, AgreementDetailsList, model.ts) live under
frontend/src/shared/; see
docs/architecture.md for the frontend structure.
These plugs demonstrate the UI SDK breadth rather than a specific business flow.
Like the agreement plugs, they are organised per entity — one PlugRouter
file each: portal.py
(top-level: the learn-extensions group with examples and guide, add),
orders.py,
subscriptions.py,
assets.py,
accounts.py, and the
agreements add plug in agreements.py:
| Plug id / socket | Frontend module | Demonstrates |
|---|---|---|
learn-extensions — portal |
— (no bundle) | A NavigationPlug container: a href-less navigation grouping node whose id derives the nested socket portal.learn-extensions for the two plugs below |
examples — portal.learn-extensions |
modules/examples/ |
A multi-tab React app (react-router) touring the UI SDK: Introduction, Basics, Context (useMPTContext), API calls (the http client feeding a server-driven Grid via useGridWithRql + RQL), UI elements (buttons, inputs, selects, toggles, date pickers, grids, entity references), and Modals (the socketless dialog / wizard round-trip via useMPTModal().open()) |
guide — portal.learn-extensions |
modules/guide/ |
Rendering bundled Markdown with InlineMarkdown (esbuild .md text loader) |
add-<socket> (one per socket) |
modules/add-*/ |
A "plug here" scaffold shown on every remaining Portal socket, so the full set of sockets is covered |
The add-* plugs cover many near-identical sockets without duplicating logic:
each is a thin module directory whose index.tsx mounts the shared
AddPlugShowcase
component with its socket passed as a prop, and each maps to one add_plug(...)
line in the matching per-entity router (the helper lives in
plugs/common.py).
backend/migrations/ contains example data and schema
migrations. They are intentionally fake placeholders; see
docs/migrations.md.