The four ways in
- Version
- 4.0 preview
- Updated
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
| Surface | Entry point | Who calls it | Authenticates with |
|---|---|---|---|
| Browser (admin) | index.php?option=com_configbox&controller=admin*&task=… | The admin UI's own JavaScript | Host session + core.manage |
| Browser (storefront) | /cb-api/{controller}/{task} | The storefront's JavaScript | Host session (public tasks) |
| HTTP API | /cb-api/v1/{entity}[/{id}] | Any program, anywhere | Bearer token + area:level scopes |
| MCP | configbox:mcp over stdio, or POST /cb-api/v1/mcp | An AI assistant — locally, or anywhere | stdio: nothing, shell access is the boundary · HTTP: the same bearer token and scopes as the HTTP API |
| CLI | configbox:* commands | Deploys, cron, an operator | Nothing — 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
| Surface | Adds |
|---|---|
| Browser (admin) | core.manage check, reads the form post, renders or answers JSON |
| Browser (storefront) | Session-scoped cart and configuration, no permission model |
| HTTP API | Bearer token and scope, HTTP status codes, RFC 9457 problem bodies |
| MCP | A narrower writable entity set, --read-only, prose errors instead of typed ones |
| CLI | Argument 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 CLI — nothing, 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, soconfigbox:run-taskwas refused on everyadmin*controller however--userwas set — while MCP, on the same shell-access boundary, wrote freely. Nobody had decided that; it fell out ofisAuthorized()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.--useris 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}, asapplication/json4xx/5xx→ RFC 9457 problem details, asapplication/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:
| Thrown | Means | HTTP |
|---|---|---|
KenedoValidationException | the caller's data was refused | 422 |
KenedoNotFoundException | no such entity or record | 404 |
KenedoSystemException | something broke on our side | 500 |
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.
7. Where to read next
| You want | Read |
|---|---|
| To call the HTTP API | com_configbox_entity_api.md |
| How the HTTP API is built | com_configbox_http_api.md |
| To run or extend the MCP server | com_configbox_mcp_server.md |
| How MCP is built | com_configbox_mcp_server_internals.md |
| The CLI suite | com_configbox_cli_commands.md |
| Browser controller tasks, in detail | com_configbox_mvc_tasks.md |
| Calling them from JavaScript | com_configbox_frontend_requests.md |
| The configurator/cart runtime calls | com_configbox_runtime_api.md |
| The response contract | com_configbox_api_contract.md |
| Where field descriptions come from | com_configbox_type_generation.md |