Skip to main content
Version: 3.x

The four ways in

Version
3.x
Updated
View markdown

CBX can be driven from four directions. They look different, authenticate differently and are documented separately — but below a certain line they are the same code, and knowing where that line falls is most of what you need to reason about any of them.

This is the map. Each surface has its own detailed document; this one says which to read and why.

A diagram of the same thing: com_configbox_api_wiring.html — open it in a browser. The AI surface has its own picture page, com_configbox_ai_surface.html: what MCP is, how a conversation runs, the two transports converging, and the scope grid.


1. The four surfaces

SurfaceEntry pointWho calls itAuthenticates with
Browser (admin)index.php?option=com_configbox&controller=admin*&task=…The admin UI's own JavaScriptHost session + core.manage
Browser (storefront)/cb-api/{controller}/{task}The storefront's JavaScriptHost session (public tasks)
HTTP API/cb-api/v1/{entity}[/{id}]Any program, anywhereBearer token + area:level scopes
MCPconfigbox:mcp over stdio, or POST /cb-api/v1/mcpAn AI assistant — locally, or anywherestdio: nothing, shell access is the boundary · HTTP: the same bearer token and scopes as the HTTP API
CLIconfigbox:* commandsDeploys, cron, an operatorNothing — shell access is the boundary

Five rows for four directions, because the browser arrives as two different things: an authenticated operator editing the catalog, and an anonymous shopper configuring a product. They share a router and share nothing else.

Deliberately off this map: controllers/testautomation.php, the E2E suite's fixture-seeding surface. It authenticates per request with superuser credentials and exists only for test automation — not a way in that anything besides the suite may use.

Which to reach for

  • Integrating another system — the HTTP API. It is the only surface designed to be called by something that is not on the same machine and not a browser.
  • Letting an assistant author a catalog — MCP. Same pipeline, tool-shaped, no HTTP exposure.
  • A deploy step, a migration, a scripted fix — the CLI.
  • Building admin or storefront UI — the controller tasks, through server.request().

2. Where they converge

browser (admin) browser (storefront) HTTP API MCP CLI
│ │ │ │ │
KenedoController KenedoController ConfigboxController Configbox Configbox
::store/delete/… (public tasks) Apiv1 McpHelper CliHelper
│ │ │ │ │
└──────────────────────┴────────┬───────────┴──────────────┘ │
│ │
ConfigboxEntityApiHelper │
· the entity registry │
· runStorePipeline() ←─────────────────────────────────┘
│ (run-task reaches a controller)
KenedoModel
prepareForStorage → isInsert → validateData → store → afterStore

Everything below ConfigboxEntityApiHelper is shared code, not merely shared behaviour. A fix to storage semantics made there reaches every surface at once. If you find yourself making the same fix twice, one of the two places is wrong.

Everything above it is the surface's own business, and legitimately differs: a form post has a request to read, an API call has a JSON body, an MCP tool has an arguments map. What must not differ is what happens to the resulting $data object.

The rule, stated once: a surface may translate and may refuse; it may not re-implement.


3. What each surface adds, and nothing more

SurfaceAdds
Browser (admin)core.manage check, reads the form post, renders or answers JSON
Browser (storefront)Session-scoped cart and configuration, no permission model
HTTP APIBearer token and scope, HTTP status codes, RFC 9457 problem bodies
MCPA narrower writable entity set, --read-only, prose errors instead of typed ones
CLIArgument parsing, exit codes

4. Authorization, compared

This is the part most worth understanding, because the four models are genuinely different and the differences are deliberate.

Browser (admin)KenedoController::isAuthorized() returns true for any controller whose name does not start with admin. For those that do, it asks the host for com_configbox.core.manage. There is no global gate: every task authorizes itself, so a missing call is silent.

Browser (storefront) — no permission model. The cart and the configuration belong to the session, and a foreign cart position answers 404 rather than 403, because "it exists but is not yours" is a way to count the store's carts.

HTTP API — a bearer token, checked before anything is read or written, with coarse read/write scopes. Insufficient scope is 403, not 401: the caller is authenticated and retrying with the same credentials will never help.

MCP and CLInothing, deliberately. Their trust boundary is shell access to the site. Whoever can start the MCP server or run a configbox:* command can already read the database and edit the files; a credential prompt in front of that is theatre. This is why ConfigboxEntityApiHelper contains no authorization at all — a gate there would break those callers while adding nothing to the HTTP one, which authenticates before it ever arrives.

The rule covers the admin gate too — decided 2026-07-31. This box used to record an incoherence: admin controllers ask the platform for core.manage, a CLI process has no host session user, so configbox:run-task was refused on every admin* controller however --user was set — while MCP, on the same shell-access boundary, wrote freely. Nobody had decided that; it fell out of isAuthorized() consulting a host that has no CLI concept of a user. The decision: a CLI process passes the admin gate (KenedoController::isAuthorized()), for the same reason MCP carries no gate at all. --user is unaffected — it names the acting customer, not an admin identity. A CLI test pins the rule so it cannot regress silently.


5. The response contract

Every migrated surface answers the same two shapes, and the HTTP status carries the outcome — there is no success field. See the API contract for the argument.

  • 2xx{data, meta}, as application/json
  • 4xx/5xx → RFC 9457 problem details, as application/problem+json

"Migrated" is doing a little work in that sentence: the checkout tasks still render pages, the storefront's stock link tasks survive as deprecated JS-off fallbacks, and getMissingSelections* keeps its bare array — the contract's §8 is the ledger of what is done and what remains.

MCP is the exception, and only at its own boundary: a JSON-RPC tool result has no status code and no field slot, so ConfigboxMcpHelper::flattenForTool() turns the typed exceptions back into prose. The three types map the same way everywhere else:

ThrownMeansHTTP
KenedoValidationExceptionthe caller's data was refused422
KenedoNotFoundExceptionno such entity or record404
KenedoSystemExceptionsomething broke on our side500

6. What is generated, and from where

One source describes every surface's data. A model's property definitions feed the type generator, which writes the committed artifacts, from which the API description is assembled:

models (getPropertyDefinitions, incl. apiTitle / apiDescription)

ConfigboxTypeGenerator
├── generated/records/read/*.php PHP record stubs (IDE hover)
├── generated/schemas/read|write/ JSON Schema 2020-12
└── generated/js/records.d.ts TypeScript declarations

ConfigboxOpenApiHelper (inlines the schemas)
├── openapi.json
├── configbox.postman_collection.json
└── api-reference.html

configbox:generate-types writes the first three; configbox:api:export writes the last three. Both are committed artifacts — see type generation.


You wantRead
To call the HTTP APIcom_configbox_entity_api.md
How the HTTP API is builtcom_configbox_http_api.md
To run or extend the MCP servercom_configbox_mcp_server.md
How MCP is builtcom_configbox_mcp_server_internals.md
The CLI suitecom_configbox_cli_commands.md
Browser controller tasks, in detailcom_configbox_mvc_tasks.md
Calling them from JavaScriptcom_configbox_frontend_requests.md
The configurator/cart runtime callscom_configbox_runtime_api.md
The response contractcom_configbox_api_contract.md
Where field descriptions come fromcom_configbox_type_generation.md